From cfd82af7af7ce22d73e84f44f995a65fbad592fc Mon Sep 17 00:00:00 2001 From: WovenCoast Date: Wed, 19 Feb 2025 16:22:38 +0500 Subject: [PATCH 01/24] wip: proof of concept for json translatable --- composer.json | 7 +- config/translatable.php | 14 +- src/JsonTranslatable/IsJsonTranslatable.php | 226 ++++++ .../JsonTranslatableSchema.php | 25 + src/Translatable.php | 118 ++++ src/V1/Abstract/IsTranslatable.php | 126 ++++ src/V1/Enums/LanguagesOld.php | 122 ++++ .../JsonTranslatable/IsJsonTranslatable.php | 137 ++++ .../IsJsonTranslatableOld.php | 657 ++++++++++++++++++ .../TestSupport/Factories/ArticleFactory.php | 31 + tests/TestSupport/Factories/AuthorFactory.php | 21 + tests/TestSupport/Models/Article.php | 50 ++ tests/TestSupport/Models/Author.php | 17 + ...025_02_16_140456_create_articles_table.php | 29 + ...2025_02_19_084806_create_authors_table.php | 23 + ...084900_add_author_id_to_articles_table.php | 22 + tests/Unit/ExampleTest.php | 14 - .../IsJsonTranslatableTest.php | 333 +++++++++ 18 files changed, 1956 insertions(+), 16 deletions(-) create mode 100644 src/JsonTranslatable/IsJsonTranslatable.php create mode 100644 src/JsonTranslatable/JsonTranslatableSchema.php create mode 100644 src/Translatable.php create mode 100644 src/V1/Abstract/IsTranslatable.php create mode 100644 src/V1/Enums/LanguagesOld.php create mode 100644 src/V1/JsonTranslatable/IsJsonTranslatable.php create mode 100644 src/V1/JsonTranslatable/IsJsonTranslatableOld.php create mode 100644 tests/TestSupport/Factories/ArticleFactory.php create mode 100644 tests/TestSupport/Factories/AuthorFactory.php create mode 100644 tests/TestSupport/Models/Article.php create mode 100644 tests/TestSupport/Models/Author.php create mode 100644 tests/TestSupport/database/2025_02_16_140456_create_articles_table.php create mode 100644 tests/TestSupport/database/2025_02_19_084806_create_authors_table.php create mode 100644 tests/TestSupport/database/2025_02_19_084900_add_author_id_to_articles_table.php delete mode 100644 tests/Unit/ExampleTest.php create mode 100644 tests/Unit/JsonTranslatable/IsJsonTranslatableTest.php diff --git a/composer.json b/composer.json index e7861b3..7988da3 100644 --- a/composer.json +++ b/composer.json @@ -13,10 +13,15 @@ "name": "Xylam", "email": "jailam@javaabu.com", "role": "Developer" + }, + { + "name": "FlameXode", + "email": "hi@flamexo.de", + "role": "Developer" } ], "require": { - "php": "^8.1", + "php": "^8.3", "illuminate/support": "^9.0 || ^10.0 || ^11.0" }, "require-dev": { diff --git a/config/translatable.php b/config/translatable.php index ea4a711..d145b74 100644 --- a/config/translatable.php +++ b/config/translatable.php @@ -10,5 +10,17 @@ | */ - // TODO + 'fields_ignored_for_translation' => [ + 'id', + 'translations', + 'lang', + 'created_at', + 'updated_at', + 'deleted_at', + ], + 'allowed_translation_locales' => [ + 'en' => 'English', + 'dv' => 'Dhivehi', + 'jp' => 'Japanese', + ] ]; diff --git a/src/JsonTranslatable/IsJsonTranslatable.php b/src/JsonTranslatable/IsJsonTranslatable.php new file mode 100644 index 0000000..e0a7c9d --- /dev/null +++ b/src/JsonTranslatable/IsJsonTranslatable.php @@ -0,0 +1,226 @@ +fields_ignored_for_translation, + config('translatable.fields_ignored_for_translation') + ))); + } + + /** + * Get non translatable fields without pivots and fields ignored for translation + * + * Use getAllNonTranslatables to get non translatables including pivots and fields ignored for translation + * + * @return array + */ + public function getNonTranslatables(): array + { + $all_fields = \Schema::getColumnListing($this->getTable()); + + $hide = array_merge($this->getTranslatables(), $this->getFieldsIgnoredForTranslation(), $this->getNonTranslatablePivots()); + + return array_values(array_diff($all_fields, $hide)); + } + + /** + * Get all non translatable fields + * + * @return array + */ + public function getAllNonTranslatables(): array + { + return array_merge($this->getNonTranslatables(), $this->getFieldsIgnoredForTranslation(), $this->getNonTranslatablePivots()); + } + + /** + * Check if relation is a non translatable pivot + * + * @param string $relation + * @return bool + */ + public function isNonTranslatablePivot(string $relation): bool + { + return in_array($relation, $this->getNonTranslatablePivots()); + } + + /** + * Translate a given field to the given locale + * + * If fallback is true, fallback to default locale if given locale is unavailable + * + * @param string $field + * @param string|null $locale + * @param bool $fallback + * @return mixed + */ + public function translate(string $field, ?string $locale = null, bool $fallback = true): mixed + { + // Use current app locale whenever locale isn't provided + if (is_null($locale)) { + $locale = app()->getLocale(); + } + + // If default lang just return the field normally + if ($this->isDefaultTranslationLocale($locale)) { + return $this->getAttributeValue($field); + } + + // If the locale is not allowed then return null + if (! $this->isAllowedTranslationLocale($locale)) { + return $fallback ? $this->getAttributeValue($field) : null; + } + + // If the field is not allowed then return null + if (! $this->isTranslatable($field)) { + return $fallback ? $this->getAttributeValue($field) : null; + } + + $translations = $this->getAttributeValue('translations'); + + // Check if translations for that locale exists + if (! isset($translations[$locale])) { + return $fallback ? $this->getAttributeValue($field) : null; + } + + return $translations[$locale][$field]; + } + + /** + * Check if a given locale is the current default locale + * + * @param string $locale + * @return bool + */ + public function isDefaultTranslationLocale(string $locale): bool + { + return $this->getAttributeValue('lang') == $locale; + } + + /** + * Return all the translation locales allowed in the config file + * + * @return array + */ + public function getAllowedTranslationLocales(): array + { + return array_keys(config('translatable.allowed_translation_locales')); + } + + /** + * Check if a given locale is allowed to translate to + * + * @param string $locale + * @return bool + */ + public function isAllowedTranslationLocale(string $locale): bool + { + return in_array($locale, $this->getAllowedTranslationLocales()); + } + + /** + * Check if a given field is translatable + * + * @param string $field + * @return bool + */ + public function isTranslatable(string $field): bool + { + return in_array($field, $this->getTranslatables()); + } + + /** + * Get the field and locale for a given attribute if possible + * + * 'title_en' would return ['title', 'en'] + * + * @param string $key + * @return array + */ + public function getFieldAndLocale(string $key): array + { + $locale = Str::afterLast($key, '_'); + + if (empty($locale) || (! $this->isAllowedTranslationLocale($locale))) { + return [$key, null]; + } + + $field = Str::beforeLast($key, '_'); + return [$field, $locale]; + } + + public function getAttribute($key): mixed + { + // Add support for compoships + if (is_array($key)) { //Check for multi-columns relationship + return array_map(function ($k) { + // recursive call with a string + return self::getAttribute($k); + }, $key); + } + + // translate using current app locale if possible + if ($this->isTranslatable($key)) { + return $this->translate($key); + } + + // check if is a suffixed attribute + [$field, $locale] = $this->getFieldAndLocale($key); + if ($locale && $this->isTranslatable($field)) { + return $this->translate($field, $locale, false); + } + + + // fallback to parent + return parent::getAttribute($key); + } + + public function clearTranslations(?string $locale = null): void + { + // clear all translations if none is provided + if (is_null($locale)) { + $this->translations = null; + $this->save(); + return; + } + + $translations = $this->getAttributeValue('translations'); + + if (! isset($translations[$locale])) { + return; + } + + unset($translations[$locale]); + + $this->translations = $translations; + $this->save(); + } + + /** + * Ensures that translations exist for a given locale + * + * @param string|null $locale + * @return bool + */ + public function hasTranslation(?string $locale = null): bool + { + if (is_null($locale)) { + $locale = app()->getLocale(); + } + + return isset($this->translations[$locale]); + } +} diff --git a/src/JsonTranslatable/JsonTranslatableSchema.php b/src/JsonTranslatable/JsonTranslatableSchema.php new file mode 100644 index 0000000..53fb4ce --- /dev/null +++ b/src/JsonTranslatable/JsonTranslatableSchema.php @@ -0,0 +1,25 @@ +runningUnitTests()) { +// $table->text('translations')->nullable(); +// } else { + $table->json('translations')->nullable(); +// } + + $table->string('lang')->index(); + } +} diff --git a/src/Translatable.php b/src/Translatable.php new file mode 100644 index 0000000..340e1fa --- /dev/null +++ b/src/Translatable.php @@ -0,0 +1,118 @@ + + */ + public function getAllowedTranslationLocales(): array; + + /** + * Check if given locale is allowed + * + * @param string $locale + * @return boolean + */ + public function isAllowedTranslationLocale(string $locale): bool; +} diff --git a/src/V1/Abstract/IsTranslatable.php b/src/V1/Abstract/IsTranslatable.php new file mode 100644 index 0000000..c56a844 --- /dev/null +++ b/src/V1/Abstract/IsTranslatable.php @@ -0,0 +1,126 @@ +fallback_translations = config('translatable.fallback_translations') == 'true'; + } + + /** + * Check whether to show translation fallbacks + * + * @return boolean + */ + public function shouldFallbackForTranslations(): bool + { + return $this->fallback_translations; + } + + /** + * Set whether to show translation fallbacks + */ + public function setShouldFallbackForTranslations(bool $fallback_translations): self + { + $this->fallback_translations = $fallback_translations; + return $this; + } + + /** + * Get the translation ignored fields + * + * @return array + */ + public function getFieldsIgnoredForTranslation(): array + { + return config('translatable.fields_ignored_for_translation'); + } + + /** + * Check is default translation locale + * + * @param string $locale + * @return boolean + */ + public function isDefaultTranslationLocale(string $locale): bool + { + return strtolower($this->getDefaultTranslationLocale()) == strtolower($locale); + } + + /** + * Get default translation locale + * + * @return string + */ + public function getDefaultTranslationLocale(): string + { + return app()->getLocale(); + } + + /** + * Get allowed translation locales + * + * @return array + */ + public function getAllowedTranslationLocales(): array + { + return array_keys(config('translatable.allowed_translation_locales')); + } + + /** + * Check if given locale is allowed + * + * @param string $locale + * @return boolean + */ + public function isAllowedTranslationLocale(string $locale): bool + { + return in_array($locale, $this->getAllowedTranslationLocales()); + } + + /** + * Get all pivots that must not be translatable + * + * @return array + */ + abstract public function getNonTranslatablePivots(): array; + + public function getNonTranslatables() { + $all_fields = \Schema::getColumnListing($this->getTable()); + + $hide = array_merge($this->getTranslatables(), $this->getFieldsIgnoredForTranslation()); + + return array_values(array_diff($all_fields, $hide)); + } + + /** + * Get all pivots and attributes that must not be translatable + * + * @return array + */ + public function getAllNonTranslatables(): array + { + return array_merge( + $this->getNonTranslatablePivots(), + $this->getNonTranslatables() + ); + } + + /** + * Check if is a non translatable pivot + * + * @param string $relation + * @return boolean + */ + public function isNonTranslatablePivot(string $relation): bool + { + return in_array($relation, $this->getNonTranslatablePivots()); + } +} diff --git a/src/V1/Enums/LanguagesOld.php b/src/V1/Enums/LanguagesOld.php new file mode 100644 index 0000000..6f0530f --- /dev/null +++ b/src/V1/Enums/LanguagesOld.php @@ -0,0 +1,122 @@ + __('Dhivehi'), + static::EN => __('English'), + ]; + } + + protected static function getFlag($key): string + { + return static::$flags[$key] ?? ''; + } + + public static function flagUrl($key): string + { + return Flags::getFlagUrl(self::getFlag($key)); + } + + public static function getLocaleFlag($current_locale = null, $opposite = false): string + { + if (!$current_locale) { + $current_locale = app()->getLocale(); + } + + $locale = $opposite ? self::getOppositeLocale() : $current_locale; + + return self::flagUrl($locale); + } + + public static function getOppositeLocaleFlag($current_locale = null): string + { + return self::getLocaleFlag($current_locale, true); + } + + public static function getDefaultTranslationLocale(): string + { + return config('translations.default_translation_locale'); + } + + public static function isRtl($value): bool + { + return $value == self::DV; + } + + public static function getDirection($current_locale = null): string + { + if (!$current_locale) { + $current_locale = app()->getLocale(); + } + + return self::isRtl($current_locale) ? 'rtl' : 'ltr'; + } + + public static function getOppositeLocale($currentLocale = null): string + { + if (!$currentLocale) { + $currentLocale = app()->getLocale(); + } + + return $currentLocale == self::DV ? self::EN : self::DV; + } + + public static function translateCurrentRoute(): string + { + $current_route = Route::getCurrentRoute()->getName(); + + $route_params = Route::getCurrentRoute()->parameters(); + + $switch_to = self::getOppositeLocale(); + + $route_params['language'] = $switch_to; + + return route($current_route, $route_params); + } + + public static function getLocalizedUrl($translatable, $locale = null): string + { + if (!$locale) { + $locale = self::getOppositeLocale(); + } + + $url = null; + + if ($translatable instanceof Translatable) { + $url = $translatable->getLocalizedUrl($locale); + } elseif ($translatable) { + $url = public_url('/' . $locale . '/' . ltrim($translatable, '/')); + } + + return $url ?: public_url('/' . $locale); + } + + /** + * Set current session locale + * + * @return string + */ + public static function getSessionLocale(): string + { + return session()->get('language', static::getDefaultTranslationLocale()); + } +} diff --git a/src/V1/JsonTranslatable/IsJsonTranslatable.php b/src/V1/JsonTranslatable/IsJsonTranslatable.php new file mode 100644 index 0000000..3a80948 --- /dev/null +++ b/src/V1/JsonTranslatable/IsJsonTranslatable.php @@ -0,0 +1,137 @@ +getTable()); + + $hide = array_merge($this->getTranslatables(), $this->getFieldsIgnoredForTranslation()); + + return array_values(array_diff($all_fields, $hide)); + } + + public function translate(string $field, ?string $locale = null, bool $fallback = true): mixed + { + if (! $locale && $fallback) { + $locale = app()->getLocale(); + } + + if (! $locale || ! $this->isAllowedTranslationLocale($locale)) { + return null; + } + + if ($this->isPrimaryLocale($locale)) { + // for default locale just use the direct field value + $value = parent::getAttribute($field); + } else { + // for other locales, use the appropriate translation for that locale + $value = $this->translations[$locale][$field] ?? null; + + if ($value && $this->hasCast($field) && is_string($value)) { + $value = $this->castAttribute($field, $value); + } + + // fallback to default if translation missing + if (empty($value) && $fallback) { + $value = parent::getAttribute($field); + } + } + + return $value; + } + + public function isPrimaryLocale(string $locale): bool + { + return $locale == $this->getAttribute('lang'); + } + + public function isTranslatable(string $field): bool + { + return in_array($field, $this->getTranslatables()); + } + + public function clearTranslations(?string $locale = null): void + { + $this->translation = null; + } + + public function hasTranslation(?string $locale = null): bool + { + if (is_null($locale)) { + $locale = app()->getLocale(); + } + + // if the row is already in the correct language + if ($this->isPrimaryLocale($locale)) { + return true; + } + + // if the provided locale is not allowed we can ignore + if ($this->isAllowedTranslationLocale($locale)) { + return false; + } + + // check if translation exists for locale + $values = $this->translations[$locale]; + return ! empty($values); + } + + + public function getFieldAndLocale(string $key) + { + $locale = Str::afterLast($key, '_'); + + if (empty($locale) || (! $this->isAllowedTranslationLocale($locale))) { + return [$key, null]; + } + + $field = Str::beforeLast($locale, '_'); + return [$key, $field]; + } + + /** + * Get an attribute from the model. + * + * @param string $key + * + * @return mixed + */ + public function getAttribute($key): mixed + { + // Add support for compoships + if (is_array($key)) { //Check for multi-columns relationship + return array_map(function ($k) { + return parent::getAttribute($k); + }, $key); + } + + + // translate if translatable + if ($this->isTranslatable($key)) { + return $this->translate($key, null, $this->shouldFallbackForTranslations()); + } + + // check if is a suffixed attribute + [$field, $locale] = $this->getFieldAndLocale($key); + + if ($locale && $this->isTranslatable($field)) { + return $this->translate($field, $locale, false); + } + + // fallback to parent + if (($attr = parent::getAttribute($key)) !== null) { + return $attr; + } + + return null; + } +} diff --git a/src/V1/JsonTranslatable/IsJsonTranslatableOld.php b/src/V1/JsonTranslatable/IsJsonTranslatableOld.php new file mode 100644 index 0000000..c174698 --- /dev/null +++ b/src/V1/JsonTranslatable/IsJsonTranslatableOld.php @@ -0,0 +1,657 @@ +lang) { + $model->lang = app()->getLocale(); + } + }); + } + + /** + * Check whether to show translation fallbacks + * + * @return boolean + */ + public function shouldFallbackForTranslations(): bool + { + return $this->fallback_translations; + } + + /** + * Set to show translation fallbacks + * + * @return void + */ + public function showTranslationFallbacks(): void + { + $this->fallback_translations = true; + } + + /** + * Set to not show translation fallbacks + * + * @return void + */ + public function dontShowTranslationFallbacks(): void + { + $this->fallback_translations = false; + } + + /** + * A search scope + * + * @param $query + * @param $field + * @param $search + * @param null $locale + * @return mixed + */ + public function scopeTranslationsSearch($query, $field, $search, $locale = null): mixed + { + if (! $locale) { + $locale = app()->getLocale(); + } + + // case insensitive search https://sarav.co/case-insensitive-search-in-mysql-json-columns + $query->where(function ($query) use ($search, $locale, $field) { + $query->whereRaw('lower(json_unquote(json_extract(`translations`, \'$."' . $field . '"\'))) like ?', ['%' . strtolower($search) . '%']) + ->orWhere($field, 'like', '%' . $search . '%'); + }); + + return $query; + } + + /** + * Locale scope to return where lang == current locale + * + * @param $query + * @param string|null $locale + * @return mixed + */ + public function scopeOfLocale($query, string $locale = null): mixed + { + if (! $locale) { + $locale = app()->getLocale(); + } + + return $query->where('lang', $locale) + ->orWhereNotNull('translations'); + } + + public function scopeNotHiddenOfLocale($query, string $locale = null) + { + if (! $locale) { + $locale = app()->getLocale(); + } + + return $query->where('lang', $locale) + ->orWhere(function ($query) { + return $query->whereNotNull('translations') + ->where('hide_translation', false); + }); + } + + /** + * Translate the given field to given locale. + * Fall back to default if no translation + * + * @param $field + * @param null $locale + * @param bool $fallback + * @return ?string + */ + public function translate($field, $locale = null, bool $fallback = true): ?string + { + if (! $locale) { + $locale = app()->getLocale(); + } + + if ($this->isPrimaryLocale($locale)) { + // for default locale, use the direct field value + $value = parent::getAttribute($field); + } else { + // for other locales, use translations + $value = $this->translations[$field] ?? null; + + if ($value && $this->hasCast($field) && is_string($value)) { + $value = $this->castAttribute($field, $value); + } + + // fallback to default if translation missing + if (empty($value) && $fallback) { + $value = parent::getAttribute($field); + } + } + + return $value; + } + + /** + * Translate the given field to given locale. + * Fall back to default if no translation + * + * @param $field + * @return string + */ + public function translateField($field): string + { + return $this->translate($field); + } + +// /** +// * Get the translatable fields +// * +// * @return array +// */ +// public function getTranslatables(): array +// { +// return property_exists($this, 'translatable') ? $this->translatable : []; +// } + + /** + * Get the non translatable fields + * + * @return array + */ + public function getNonTranslatables(): array + { + $all_fields = \Schema::getColumnListing($this->getTable()); + + $hide = array_merge($this->getTranslatables(), $this->getFieldsIgnoredForTranslation()); + + return array_values(array_diff($all_fields, $hide)); + } + + /** + * Check whether the given field is translatable + * + * @param string $field + * @return boolean + */ + public function isTranslatable(string $field): bool + { + return in_array($field, $this->getTranslatables()); + } + + /** + * Set translation for given attribute name + * + * @param string $field + * @param string $locale + * @param string $translation + * @return void + */ + public function setTranslation(string $field, string $locale, string $translation): void + { + if (! $this->isTranslatable($field)) { + return; + } + + if (! $this->isAllowedTranslationLocale($locale)) { + return; + } + + if ($this->isPrimaryLocale($locale)) { + // directly save to the db field value if default locale + parent::setAttribute($field, $translation); + } else { + // for other locales, save to the translations + $this->setTranslationAttributeValue($field, $locale, $translation); + + // if it's a new model and the default value is not set, set the default + if ((! $this->exists) && (! ($this->attributes[$field] ?? null))) { + parent::setAttribute($field, $translation); + } + } + } + + /** + * Check if is primary locale + * + * @param null $locale + * @return bool + */ + public function isPrimaryLocale($locale = null): bool + { + if (! $locale) { + $locale = app()->getLocale(); + } + + return empty($this->lang) || $this->lang == $locale; + } + + /** + * Set translation attribute value + * + * @param $attribute + * @param $locale + * @param string $translation + */ + public function setTranslationAttributeValue($attribute, $locale, string $translation): void + { + $translations = Arr::wrap($this->translations); + + $translations[$attribute] = $translation; + + $this->translations = $translations; + } + + /** + * Clear the translations for the given locale or all + * + */ + public function clearTranslations(): void + { + $this->translation = null; + } + + /** + * Check if has Translations + * + * @param null $locale + * @return bool + */ + public function hasTranslations($locale = null): bool + { + if (! $locale) { +// $locale = $this->getDefaultTranslationLocale(); + } + + if ($this->lang == $locale) { + return true; + } + + return ! empty($this->translations); + } + + /** + * Fill translations in bulk + * + * @param array $translations + * @param null $locale + * @return mixed + */ + public function fillTranslations(array $translations, $locale = null): mixed + { + if (! $locale) { + $locale = app()->getLocale(); + } + + foreach ($translations as $field => $value) { + // check whether the attribute is translatable + if ($this->isTranslatable($field)) { + $this->setTranslation($field, $locale, $value); + } else { + // check whether the attribute is suffixed + [$key, $key_locale] = $this->getFieldAndLocale($field); + + if ($key_locale && $this->isTranslatable($key)) { + $this->setTranslation($key, $key_locale, $value); + } + } + } + + return $this; + } +// +// /** +// * Returns the url +// * +// * @param string $action +// * @param string|null $locale +// * @param string $namespace +// * @return string +// */ +// public function url(string $action = 'show', string $locale = null, string $namespace = 'admin'): string +// { +// if (! $locale) { +// $locale = app()->getLocale(); +// } +// +// $controller = Str::lower(Str::plural(Str::kebab(class_basename(get_class($this))))); +// $controller_action = $namespace . '.' . $controller . '.' . $action; +// +// $params = []; +// +// $params[] = $locale ?: app()->getLocale(); +// +// if (! in_array($action, ['index', 'store', 'create', 'trash'])) { +// $params[] = $this->id; +// } +// +// $url = URL::route($controller_action, $params); +// +// return $url; +// } + + /** + * Model Override functions + * -------------------------. + */ + + /** + * Convert the model's attributes to an array. + * + * @return array + */ + public function attributesToArray(): array + { + $attributes = parent::attributesToArray(); + + return $this->addTranslatableAttributesToArray($attributes); + } + + /** + * Add the translatable attributes to the attributes array. + * + * @param array $attributes + * @return array + */ + protected function addTranslatableAttributesToArray(array $attributes): array + { + foreach ($this->getTranslatables() as $key) { + if (! isset($attributes[$key])) { + continue; + } + + $attributes[$key] = $this->translate($key); + } + + return $attributes; + } + + /** + * Get an attribute from the model. + * + * @param string $key + * + * @return mixed + */ + public function getAttribute($key): mixed + { + // Add support for compoships + if (is_array($key)) { //Check for multi-columns relationship + return array_map(function ($k) { + return parent::getAttribute($k); + }, $key); + } + + + // translate if translatable + if ($this->isTranslatable($key)) { + return $this->translate($key, null, $this->shouldFallbackForTranslations()); + } + + // check if is a suffixed attribute + [$field, $locale] = $this->getFieldAndLocale($key); + + if ($locale && $this->isTranslatable($field)) { + return $this->translate($field, $locale, false); + } + + // fallback to parent + if (($attr = parent::getAttribute($key)) !== null) { + return $attr; + } + + return null; + } + + /** + * Get the value of an attribute using its mutator for array conversion. + * + * @param string $key + * @param mixed $value + * @return mixed + */ + protected function mutateAttributeForArray($key, $value): mixed + { + // check if is a suffixed attribute + [$field, $locale] = $this->getFieldAndLocale($key); + + if ($locale && $this->isTranslatable($field)) { + return $this->{$key}; + } + + return parent::mutateAttributeForArray($key, $value); + } + + /** + * Sets attribute value + * + * @param $key + * @param $value + * @return mixed + */ + public function setAttribute($key, $value): mixed + { + + // First we will check for the presence of a mutator for the set operation + // which simply lets the developers tweak the attribute as it is set on + // the model, such as "json_encoding" an listing of data for storage. + if ($this->hasSetMutator($key)) { + return $this->setMutatedAttributeValue($key, $value); + } + + // Save the translation if is translatable + if ($this->isTranslatable($key)) { + $this->setTranslation($key, app()->getLocale(), $value); + return $this; + } + + // check if is a suffixed attribute + [$field, $locale] = $this->getFieldAndLocale($key); + + if ($locale && $this->isTranslatable($field)) { + $this->setTranslation($field, $locale, $value); + return $this; + } + + // parent call. + return parent::setAttribute($key, $value); + } + + /** + * Mutate a translated attribute value + * + * @param string $field + * @param mixed $value + */ + public function mutateTranslationAttributeValue(string $field, mixed $value): void + { + if ($this->isJsonCastable($field) && $value) { + $value = $this->castAttributeAsJson($field, $value); + } + + if (! $this->isTranslatable($field)) { + $this->attributes[$field] = $value; + return; + } + + $locale = app()->getLocale(); + + if ($this->isDefaultTranslationLocale($locale)) { + $this->attributes[$field] = $value; + } else { + $this->setTranslationAttributeValue($field, $locale, $value); + + // if it's a new model and the default value is not set, set the default + if ((! $this->exists) && (! parent::getAttribute($value))) { + $this->attributes[$field] = $value; + } + } + } + + /** + * Get the current lang for the record + * + * @return string + */ + public function getCurrentLangAttribute(): string + { + $locale = app()->getLocale(); + + if ($this->hasTranslations($locale)) { + return $locale; + } + + return $this->lang; + } + + /** + * + * @param $locale + * @return $this|bool + */ + public function getTranslation($locale): bool|static + { + if ($this->lang == $locale) { + return $this; + } + + if ($this->lang != $locale && $this->translations) { + return true; + } + } + + public function isTranslationHidden($locale): bool + { + return $this->lang != $locale && $this->hide_translation; + } + + /** + * Get the field and locale for magic attribute + * + * @param string $attribute + * @return array [field, locale] + */ + public function getFieldAndLocale(string $attribute): array + { + $locale = Str::afterLast($attribute, '_'); + + if (empty($locale) || (! $this->localeExists($locale))) { + return [$attribute, null]; + } + + $field = Str::beforeLast($attribute, '_'); + + return [$field, $locale]; + } + + /** + * Check if the locale exists in the configuration + * + * @param string $locale + * @return bool + */ + public function localeExists(string $locale): bool + { + return in_array($locale, $this->getAllowedTranslationLocales()); + } + + /** + * Get the fillable attributes for the model. + * + * @return array + */ + public function getFillableTranslatables(): array + { + $translatables = $this->getTranslatables(); + $fillables = $this->getFillable(); + + $translatable_fillables = array_intersect($translatables, $fillables); + $language_codes = $this->getAllowedTranslationLocales(); + + $suffixed_fillables = []; + + foreach ($translatable_fillables as $fillable) { + foreach ($language_codes as $code) { + $suffixed_fillables[] = $fillable . '_' . $code; + } + } + + return $suffixed_fillables; + } + + /** + * Get the fillable translatable attributes of a given array. + * + * @param array $attributes + * @return array + */ + protected function fillableTranslatablesFromArray(array $attributes): array + { + return array_intersect_key($attributes, array_flip($this->getFillableTranslatables())); + } + + /** + * First fill the suffixed translatables + * Then fill the main attributes + * + * @param array $attributes + * @return $this + * + * @throws MassAssignmentException + */ + public function fill(array $attributes): static + { + $fillable_translatables = $this->fillableTranslatablesFromArray($attributes); + + foreach ($fillable_translatables as $key => $value) { + $this->setAttribute($key, $value); + } + + return parent::fill($attributes); + } + + /** + * Add appends to translatable fields + * + * @param array $appends + * @return array + */ + public function addTranslationAppends(array $appends): array + { + $language_codes = $this->getAllowedTranslationLocales(); + + $translatables = $this->getTranslatables(); + $converted_appends = $appends; + + foreach ($translatables as $field) { + foreach ($language_codes as $code) { + $converted_appends[$field . '_' . $code] = [$field]; + } + } + + return $converted_appends; + } + + public function getIsTranslationAttribute(): bool + { + return $this->lang != app()->getLocale(); + } +} diff --git a/tests/TestSupport/Factories/ArticleFactory.php b/tests/TestSupport/Factories/ArticleFactory.php new file mode 100644 index 0000000..c245776 --- /dev/null +++ b/tests/TestSupport/Factories/ArticleFactory.php @@ -0,0 +1,31 @@ + fake()->title(), + 'slug' => fake()->slug(), + 'body' => fake()->paragraph(5), + 'lang' => fake()->locale() + ]; + } + + public function withAuthor(): self + { + return $this->state(function (array $attributes) { + return [ + 'author_id' => Author::factory(), + ]; + }); + } +} diff --git a/tests/TestSupport/Factories/AuthorFactory.php b/tests/TestSupport/Factories/AuthorFactory.php new file mode 100644 index 0000000..a6b9c69 --- /dev/null +++ b/tests/TestSupport/Factories/AuthorFactory.php @@ -0,0 +1,21 @@ + $this->faker->name(), + 'created_at' => Carbon::now(), + 'updated_at' => Carbon::now(), + ]; + } +} diff --git a/tests/TestSupport/Models/Article.php b/tests/TestSupport/Models/Article.php new file mode 100644 index 0000000..a14831e --- /dev/null +++ b/tests/TestSupport/Models/Article.php @@ -0,0 +1,50 @@ + 'array', + ]; + + protected static function newFactory(): ArticleFactory + { + return new ArticleFactory(); + } + + public function getTranslatables(): array + { + return [ + 'title', + 'body', + ]; + } + + public function getNonTranslatablePivots(): array + { + return [ + 'author_id' + ]; + } + + public function getDefaultTranslationLocale(): string + { + // TODO: Implement getDefaultTranslationLocale() method. + } +} diff --git a/tests/TestSupport/Models/Author.php b/tests/TestSupport/Models/Author.php new file mode 100644 index 0000000..9098266 --- /dev/null +++ b/tests/TestSupport/Models/Author.php @@ -0,0 +1,17 @@ +id(); + + $table->string('title'); + $table->string('slug'); + $table->text('body'); + + JsonTranslatableSchema::columns($table); + + $table->timestamps(); + $table->softDeletes(); + }); + } + + public function down() + { + Schema::dropIfExists('articles'); + } +} diff --git a/tests/TestSupport/database/2025_02_19_084806_create_authors_table.php b/tests/TestSupport/database/2025_02_19_084806_create_authors_table.php new file mode 100644 index 0000000..61e9a72 --- /dev/null +++ b/tests/TestSupport/database/2025_02_19_084806_create_authors_table.php @@ -0,0 +1,23 @@ +id(); + + $table->string('name'); + + $table->timestamps(); + }); + } + + public function down(): void + { + Schema::dropIfExists('authors'); + } +}; diff --git a/tests/TestSupport/database/2025_02_19_084900_add_author_id_to_articles_table.php b/tests/TestSupport/database/2025_02_19_084900_add_author_id_to_articles_table.php new file mode 100644 index 0000000..06dc0c4 --- /dev/null +++ b/tests/TestSupport/database/2025_02_19_084900_add_author_id_to_articles_table.php @@ -0,0 +1,22 @@ +foreignIdFor(Author::class); + }); + } + + public function down(): void + { + Schema::table('articles', function (Blueprint $table) { + $table->dropForeign('articles_author_id_foreign'); + }); + } +}; diff --git a/tests/Unit/ExampleTest.php b/tests/Unit/ExampleTest.php deleted file mode 100644 index d719cd3..0000000 --- a/tests/Unit/ExampleTest.php +++ /dev/null @@ -1,14 +0,0 @@ -assertTrue(true); - } -} diff --git a/tests/Unit/JsonTranslatable/IsJsonTranslatableTest.php b/tests/Unit/JsonTranslatable/IsJsonTranslatableTest.php new file mode 100644 index 0000000..86e249b --- /dev/null +++ b/tests/Unit/JsonTranslatable/IsJsonTranslatableTest.php @@ -0,0 +1,333 @@ +assertEquals([ + 'id', + 'translations', + 'lang', + 'created_at', + 'updated_at', + 'deleted_at', + ], $article->getFieldsIgnoredForTranslation()); + } + + /** @test */ + public function it_can_get_translatable_fields() + { + $article = new Article(); + + $this->assertEquals([ + 'title', + 'body', + ], $article->getTranslatables()); + } + + /** @test */ + public function it_can_get_non_translatable_fields() + { + $article = new Article(); + + $this->assertEquals([ + 'slug', + ], $article->getNonTranslatables()); + } + + /** @test */ + public function it_can_get_non_translatable_pivots() + { + $article = new Article(); + + $this->assertEquals([ + 'author_id', + ], $article->getNonTranslatablePivots()); + } + + /** @test */ + public function it_can_get_all_non_translatable_fields() + { + $article = new Article(); + + $this->assertEquals([ + 'slug', + 'id', + 'translations', + 'lang', + 'created_at', + 'updated_at', + 'deleted_at', + 'author_id', + ], $article->getAllNonTranslatables()); + } + + /** @test */ + public function it_can_check_if_is_a_non_translatable_pivot() + { + $article = new Article(); + + $this->assertFalse($article->isNonTranslatablePivot('id')); + $this->assertTrue($article->isNonTranslatablePivot('author_id')); + } + + /** @test */ + public function it_can_translate_field_via_translate_function() + { + $article = Article::factory()->withAuthor()->create([ + 'lang' => 'en', + 'title' => 'This is an English title', + 'slug' => 'this-is-an-english-slug', + 'body' => 'This is an English body', + 'translations' => [ + 'dv' => [ + 'title' => 'Mee dhivehi title eh', + 'slug' => 'mee-dhivehi-slug-eh', + 'body' => 'Mee dhivehi liyumeh', + ], + 'jp' => [ + 'title' => 'Kore wa taitorudesu', + 'slug' => 'kore-wa-namekujidesu', + 'body' => 'Kore wa kijidesu', + ] + ] + ]); + + $this->assertEquals('Mee dhivehi title eh', $article->translate('title', 'dv')); + $this->assertEquals('Mee dhivehi liyumeh', $article->translate('body', 'dv')); + + $this->assertEquals('Kore wa taitorudesu', $article->translate('title', 'jp')); + $this->assertEquals('Kore wa kijidesu', $article->translate('body', 'jp')); + } + + /** @test */ + public function it_can_translate_field_via_translate_function_without_fallback() + { + $article = Article::factory()->withAuthor()->create([ + 'lang' => 'en', + 'title' => 'This is an English title', + 'slug' => 'this-is-an-english-slug', + 'body' => 'This is an English body', + 'translations' => [ + 'dv' => [ + 'title' => 'Mee dhivehi title eh', + 'slug' => 'mee-dhivehi-slug-eh', + 'body' => 'Mee dhivehi liyumeh', + ], + ] + ]); + + $this->assertEquals('This is an English title', $article->translate('title', 'jp')); + $this->assertEquals('this-is-an-english-slug', $article->translate('slug', 'jp')); + $this->assertEquals('This is an English body', $article->translate('body', 'jp')); + + $this->assertNull($article->translate('title', 'jp', false)); + $this->assertNull($article->translate('slug', 'jp', false)); + $this->assertNull($article->translate('body', 'jp', false)); + } + + /** @test */ + public function it_can_translate_field_via_magic_method() + { + $article = Article::factory()->withAuthor()->create([ + 'translations' => [ + 'dv' => [ + 'title' => 'Mee dhivehi title eh', + 'slug' => 'mee-dhivehi-slug-eh', + 'body' => 'Mee dhivehi liyumeh', + ], + 'jp' => [ + 'title' => 'Kore wa taitorudesu', + 'slug' => 'kore-wa-namekujidesu', + 'body' => 'Kore wa kijidesu', + ] + ] + ]); + + $this->assertEquals('Mee dhivehi title eh', $article->title_dv); + $this->assertEquals('Mee dhivehi liyumeh', $article->body_dv); + + // slugs should not be translated + $this->assertNull($article->slug_dv); + } + + /** @test */ + public function it_can_translate_field_via_locale_change() + { + $article = Article::factory()->withAuthor()->create([ + 'translations' => [ + 'dv' => [ + 'title' => 'Mee dhivehi title eh', + 'slug' => 'mee-dhivehi-slug-eh', + 'body' => 'Mee dhivehi liyumeh', + ], + 'jp' => [ + 'title' => 'Kore wa taitorudesu', + 'slug' => 'kore-wa-namekujidesu', + 'body' => 'Kore wa kijidesu', + ] + ] + ]); + + $tmp = app()->getLocale(); + app()->setLocale('dv'); + $this->assertEquals('Mee dhivehi title eh', $article->title); + $this->assertEquals('Mee dhivehi liyumeh', $article->body); + app()->setLocale($tmp); + } + + /** @test */ + public function it_can_check_if_given_field_is_translatable() + { + $article = Article::factory()->withAuthor()->create([ + 'translations' => [ + 'dv' => [ + 'title' => 'Mee dhivehi title eh', + 'slug' => 'mee-dhivehi-slug-eh', + 'body' => 'Mee dhivehi liyumeh', + ], + 'jp' => [ + 'title' => 'Kore wa taitorudesu', + 'slug' => 'kore-wa-namekujidesu', + 'body' => 'Kore wa kijidesu', + ] + ] + ]); + + $this->assertTrue($article->isTranslatable('title')); + } + + /** @test */ + public function it_can_clear_translations_for_one_locale() + { + $article = Article::factory()->withAuthor()->create([ + 'translations' => [ + 'dv' => [ + 'title' => 'Mee dhivehi title eh', + 'slug' => 'mee-dhivehi-slug-eh', + 'body' => 'Mee dhivehi liyumeh', + ], + 'jp' => [ + 'title' => 'Kore wa taitorudesu', + 'slug' => 'kore-wa-namekujidesu', + 'body' => 'Kore wa kijidesu', + ] + ] + ]); + + $article->clearTranslations('dv'); + + // Ensure that dv is gone while jp is still there + $this->assertEmpty($article->title_dv); + $this->assertEquals('Kore wa taitorudesu', $article->title_jp); + } + + /** @test */ + public function it_can_clear_translations_for_all_locales() + { + $article = Article::factory()->withAuthor()->create([ + 'translations' => [ + 'dv' => [ + 'title' => 'Mee dhivehi title eh', + 'slug' => 'mee-dhivehi-slug-eh', + 'body' => 'Mee dhivehi liyumeh', + ], + 'jp' => [ + 'title' => 'Kore wa taitorudesu', + 'slug' => 'kore-wa-namekujidesu', + 'body' => 'Kore wa kijidesu', + ] + ] + ]); + + $article->clearTranslations(); + + $this->assertEmpty($article->title_dv); + $this->assertEmpty($article->title_jp); + } + + /** @test */ + public function it_can_check_if_any_translation_for_a_specific_locale() + { + $article = Article::factory()->withAuthor()->create([ + 'translations' => [ + 'dv' => [ + 'title' => 'Mee dhivehi title eh', + 'slug' => 'mee-dhivehi-slug-eh', + 'body' => 'Mee dhivehi liyumeh', + ], + 'jp' => [ + 'title' => 'Kore wa taitorudesu', + 'slug' => 'kore-wa-namekujidesu', + 'body' => 'Kore wa kijidesu', + ] + ] + ]); + + $this->assertFalse($article->hasTranslation('fr')); + $this->assertTrue($article->hasTranslation('dv')); + } + + /** @test */ + public function it_can_check_if_is_default_translation_locale() + { + $article = Article::factory()->withAuthor()->create([ + 'lang' => 'en', + 'translations' => [ + 'dv' => [ + 'title' => 'Mee dhivehi title eh', + 'slug' => 'mee-dhivehi-slug-eh', + 'body' => 'Mee dhivehi liyumeh', + ], + 'jp' => [ + 'title' => 'Kore wa taitorudesu', + 'slug' => 'kore-wa-namekujidesu', + 'body' => 'Kore wa kijidesu', + ] + ] + ]); + + $this->assertFalse($article->isDefaultTranslationLocale('fr')); + $this->assertTrue($article->isDefaultTranslationLocale('en')); + } + + /** @test */ + public function it_can_get_the_default_translation_locale() + { + $article = Article::factory()->withAuthor()->create([ + 'lang' => 'en', + ]); + + $this->assertFalse($article->isDefaultTranslationLocale('fr')); + $this->assertTrue($article->isDefaultTranslationLocale('en')); + } + + /** @test */ + public function it_can_get_allowed_translation_locales() + { + $article = Article::factory()->withAuthor()->create(); + + $this->assertEquals(['en', 'dv', 'jp'], $article->getAllowedTranslationLocales()); + } + + /** @test */ + public function it_can_check_if_given_locale_is_allowed() + { + $article = Article::factory()->withAuthor()->create(); + + $this->assertTrue($article->isAllowedTranslationLocale('en')); + $this->assertTrue($article->isAllowedTranslationLocale('dv')); + $this->assertFalse($article->isAllowedTranslationLocale('fr')); + } +} From 4e1413970b1f2e83ee35dc8a13ec4f48fe67fda0 Mon Sep 17 00:00:00 2001 From: WovenCoast Date: Wed, 19 Feb 2025 16:34:48 +0500 Subject: [PATCH 02/24] test: run workflow for dev branch --- .github/workflows/run-tests.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index 7918815..fff207c 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -7,6 +7,7 @@ on: push: branches: - main + - dev jobs: tests: From a0c9d54ab891d73c49f31c91b70d0b15fc7efc84 Mon Sep 17 00:00:00 2001 From: WovenCoast Date: Thu, 20 Feb 2025 13:00:24 +0500 Subject: [PATCH 03/24] feat: add db translatable --- composer.json | 2 +- config/translatable.php | 1 - src/Abstract/IsTranslatable.php | 133 +++++++ src/DbTranslatable/DbTranslatableSchema.php | 15 + src/DbTranslatable/IsDbTranslatable.php | 136 ++++++++ src/JsonTranslatable/IsJsonTranslatable.php | 129 +------ tests/TestSupport/Factories/PostFactory.php | 33 ++ tests/TestSupport/Models/Article.php | 5 - tests/TestSupport/Models/Post.php | 43 +++ .../2025_02_20_061338_create_posts_table.php | 31 ++ .../DbTranslatable/IsDbTranslatableTest.php | 326 ++++++++++++++++++ 11 files changed, 729 insertions(+), 125 deletions(-) create mode 100644 src/Abstract/IsTranslatable.php create mode 100644 src/DbTranslatable/DbTranslatableSchema.php create mode 100644 src/DbTranslatable/IsDbTranslatable.php create mode 100644 tests/TestSupport/Factories/PostFactory.php create mode 100644 tests/TestSupport/Models/Post.php create mode 100644 tests/TestSupport/database/2025_02_20_061338_create_posts_table.php create mode 100644 tests/Unit/DbTranslatable/IsDbTranslatableTest.php diff --git a/composer.json b/composer.json index 7988da3..4883b78 100644 --- a/composer.json +++ b/composer.json @@ -21,7 +21,7 @@ } ], "require": { - "php": "^8.3", + "php": "^8.1", "illuminate/support": "^9.0 || ^10.0 || ^11.0" }, "require-dev": { diff --git a/config/translatable.php b/config/translatable.php index d145b74..474fe8b 100644 --- a/config/translatable.php +++ b/config/translatable.php @@ -12,7 +12,6 @@ 'fields_ignored_for_translation' => [ 'id', - 'translations', 'lang', 'created_at', 'updated_at', diff --git a/src/Abstract/IsTranslatable.php b/src/Abstract/IsTranslatable.php new file mode 100644 index 0000000..9a1cf08 --- /dev/null +++ b/src/Abstract/IsTranslatable.php @@ -0,0 +1,133 @@ +getAllNonTranslatables to get non translatables including pivots and fields ignored for translation + * + * @return array + */ + public function getNonTranslatables(): array + { + $all_fields = \Schema::getColumnListing($this->getTable()); + + $hide = array_merge($this->getTranslatables(), $this->getFieldsIgnoredForTranslation(), $this->getNonTranslatablePivots()); + + return array_values(array_diff($all_fields, $hide)); + } + + /** + * Get all non translatable fields, including pivots and fields ignored for translation + * + * @return array + */ + public function getAllNonTranslatables(): array + { + return array_merge($this->getNonTranslatables(), $this->getFieldsIgnoredForTranslation(), $this->getNonTranslatablePivots()); + } + + /** + * Check if relation is a non translatable pivot + * + * @param string $relation + * @return bool + */ + public function isNonTranslatablePivot(string $relation): bool + { + return in_array($relation, $this->getNonTranslatablePivots()); + } + + /** + * Check if a given locale is the current default locale + * + * @param string $locale + * @return bool + */ + public function isDefaultTranslationLocale(string $locale): bool + { + return $this->getDefaultTranslationLocale() == $locale; + } + + /** + * Check if a given locale is allowed to translate to + * + * @param string $locale + * @return bool + */ + public function isAllowedTranslationLocale(string $locale): bool + { + return in_array($locale, $this->getAllowedTranslationLocales()); + } + + /** + * Return all the translation locales allowed in the config file + * + * @return array + */ + public function getAllowedTranslationLocales(): array + { + return array_keys(config('translatable.allowed_translation_locales')); + } + + /** + * Check if a given field is translatable + * + * @param string $field + * @return bool + */ + public function isTranslatable(string $field): bool + { + return in_array($field, $this->getTranslatables()); + } + + /** + * Get the field and locale for a given attribute if possible + * + * 'title_en' would return ['title', 'en'] + * + * @param string $key + * @return array + */ + public function getFieldAndLocale(string $key): array + { + $locale = Str::afterLast($key, '_'); + + if (empty($locale) || (! $this->isAllowedTranslationLocale($locale))) { + return [$key, null]; + } + + $field = Str::beforeLast($key, '_'); + return [$field, $locale]; + } + + public function getAttribute($key): mixed + { + // Add support for compoships + if (is_array($key)) { //Check for multi-columns relationship + return array_map(function ($k) { + // recursive call with a string + return self::getAttribute($k); + }, $key); + } + + // translate using current app locale if possible + if ($this->isTranslatable($key)) { + return $this->translate($key, app()->currentLocale(), false); + } + + // check if is a suffixed attribute + [$field, $locale] = $this->getFieldAndLocale($key); + if ($locale && $this->isTranslatable($field)) { + return $this->translate($field, $locale, false); + } + + // fallback to parent + return parent::getAttribute($key); + } +} diff --git a/src/DbTranslatable/DbTranslatableSchema.php b/src/DbTranslatable/DbTranslatableSchema.php new file mode 100644 index 0000000..ebf56a0 --- /dev/null +++ b/src/DbTranslatable/DbTranslatableSchema.php @@ -0,0 +1,15 @@ +foreignId('translatable_parent_id')->nullable(); + + $table->string('lang')->index(); + } +} diff --git a/src/DbTranslatable/IsDbTranslatable.php b/src/DbTranslatable/IsDbTranslatable.php new file mode 100644 index 0000000..2bde38b --- /dev/null +++ b/src/DbTranslatable/IsDbTranslatable.php @@ -0,0 +1,136 @@ +fields_ignored_for_translation, + config('translatable.fields_ignored_for_translation') + ))); + } + + public function translations() { + return $this->hasMany($this, 'translatable_parent_id'); + } + + public function defaultTranslation() { + return $this->belongsTo($this, 'translatable_parent_id'); + } + + public function isDefaultTranslation(): bool + { + return $this->getAttributeValue('translatable_parent_id') !== null; + } + + public function translate(string $field, ?string $locale = null, bool $fallback = true): mixed + { + // Use current app locale whenever locale isn't provided + if (is_null($locale)) { + $locale = app()->getLocale(); + } + + // If default lang just return the field normally + if ($this->isDefaultTranslationLocale($locale)) { + return $this->getAttributeValue($field); + } + + // If the locale is not allowed then return null + if (! $this->isAllowedTranslationLocale($locale)) { + return $fallback ? $this->getAttributeValue($field) : null; + } + + // If the field is not in the translatable fields list then return null + if (! $this->isTranslatable($field)) { + return $fallback ? $this->getAttributeValue($field) : null; + } + + if ($this->isDefaultTranslation()) { + // if there's no parent, this is the main one. get translations using the defined relation + $translation = $this->translations()->where('lang', $locale)->first(); + } else { + // otherwise it's already a translation, get all translations including the parent + $translation = self::query()->where([ + 'id' => $this->translatable_parent_id, + 'lang' => $locale, + ])->orWhere([ + 'parent_id' => $this->translatable_parent_id, + 'lang' => $locale, + ])->first(); + } + + // fallback if the translation doesn't exist + if (! $translation) { + return $fallback ? $this->getAttributeValue($field) : null; + } + + return $translation->getAttributeValue($field); + } + + public function getDefaultTranslationLocale(): string + { + // yes this may not be the correct default translation locale + // but we do this check to see if we need to fetch another row + // from the database. + return $this->getAttributeValue('lang'); + } + + public function clearTranslations(?string $locale = null): void + { + if (is_null($locale)) { + // nuke all except the main one + $parent_id = $this->isDefaultTranslation() ? $this->id : $this->translatable_parent_id; + self::query()->where('translatable_parent_id', $parent_id)->forceDelete(); + } else { + // check the current one lang, if it's correct delete it + if ($this->lang == $locale) { + $this->forceDelete(); + } else { + if ($this->isDefaultTranslation()) { + $translation = $this->translations()->where('lang', $locale); + $translation->forceDelete(); + } else { + $translation = self::query()->where([ + 'translatable_parent_id' => $this->translatable_parent_id, + 'lang' => $locale, + ]); + $translation->forceDelete(); + } + } + } + } + + public function hasTranslation(?string $locale = null): bool + { + if (is_null($locale)) { + $locale = app()->getLocale(); + } + + // if the current one is the correct lang no need to fetch from database + if ($this->lang == $locale) { + return true; + } + + if ($this->isDefaultTranslation()) { + $translation = $this->translations()->where('lang', $locale); + return $translation->exists(); + } else { + $translation = self::query()->where([ + 'translatable_parent_id' => $this->translatable_parent_id, + 'lang' => $locale, + ])->orWhere([ + 'id' => $this->translatable_parent_id, + 'lang' => $locale, + ]); + return $translation->exists(); + } + } +} diff --git a/src/JsonTranslatable/IsJsonTranslatable.php b/src/JsonTranslatable/IsJsonTranslatable.php index e0a7c9d..ef0c4e7 100644 --- a/src/JsonTranslatable/IsJsonTranslatable.php +++ b/src/JsonTranslatable/IsJsonTranslatable.php @@ -2,9 +2,12 @@ namespace Javaabu\Translatable\JsonTranslatable; use Illuminate\Support\Str; +use Javaabu\Translatable\Abstract\IsTranslatable; trait IsJsonTranslatable { + use IsTranslatable; + public array $fields_ignored_for_translation = ['id', 'translations', 'lang']; /** @@ -20,43 +23,6 @@ public function getFieldsIgnoredForTranslation(): array ))); } - /** - * Get non translatable fields without pivots and fields ignored for translation - * - * Use getAllNonTranslatables to get non translatables including pivots and fields ignored for translation - * - * @return array - */ - public function getNonTranslatables(): array - { - $all_fields = \Schema::getColumnListing($this->getTable()); - - $hide = array_merge($this->getTranslatables(), $this->getFieldsIgnoredForTranslation(), $this->getNonTranslatablePivots()); - - return array_values(array_diff($all_fields, $hide)); - } - - /** - * Get all non translatable fields - * - * @return array - */ - public function getAllNonTranslatables(): array - { - return array_merge($this->getNonTranslatables(), $this->getFieldsIgnoredForTranslation(), $this->getNonTranslatablePivots()); - } - - /** - * Check if relation is a non translatable pivot - * - * @param string $relation - * @return bool - */ - public function isNonTranslatablePivot(string $relation): bool - { - return in_array($relation, $this->getNonTranslatablePivots()); - } - /** * Translate a given field to the given locale * @@ -84,7 +50,7 @@ public function translate(string $field, ?string $locale = null, bool $fallback return $fallback ? $this->getAttributeValue($field) : null; } - // If the field is not allowed then return null + // If the field is not in the translatable fields list then return null if (! $this->isTranslatable($field)) { return $fallback ? $this->getAttributeValue($field) : null; } @@ -100,94 +66,21 @@ public function translate(string $field, ?string $locale = null, bool $fallback } /** - * Check if a given locale is the current default locale + * Gets default translation locale * - * @param string $locale - * @return bool + * @return string */ - public function isDefaultTranslationLocale(string $locale): bool + public function getDefaultTranslationLocale(): string { - return $this->getAttributeValue('lang') == $locale; + return $this->getAttributeValue('lang'); } /** - * Return all the translation locales allowed in the config file + * Clear translations for a given language or for all languages if none is given * - * @return array - */ - public function getAllowedTranslationLocales(): array - { - return array_keys(config('translatable.allowed_translation_locales')); - } - - /** - * Check if a given locale is allowed to translate to - * - * @param string $locale - * @return bool - */ - public function isAllowedTranslationLocale(string $locale): bool - { - return in_array($locale, $this->getAllowedTranslationLocales()); - } - - /** - * Check if a given field is translatable - * - * @param string $field - * @return bool - */ - public function isTranslatable(string $field): bool - { - return in_array($field, $this->getTranslatables()); - } - - /** - * Get the field and locale for a given attribute if possible - * - * 'title_en' would return ['title', 'en'] - * - * @param string $key - * @return array + * @param string|null $locale + * @return void */ - public function getFieldAndLocale(string $key): array - { - $locale = Str::afterLast($key, '_'); - - if (empty($locale) || (! $this->isAllowedTranslationLocale($locale))) { - return [$key, null]; - } - - $field = Str::beforeLast($key, '_'); - return [$field, $locale]; - } - - public function getAttribute($key): mixed - { - // Add support for compoships - if (is_array($key)) { //Check for multi-columns relationship - return array_map(function ($k) { - // recursive call with a string - return self::getAttribute($k); - }, $key); - } - - // translate using current app locale if possible - if ($this->isTranslatable($key)) { - return $this->translate($key); - } - - // check if is a suffixed attribute - [$field, $locale] = $this->getFieldAndLocale($key); - if ($locale && $this->isTranslatable($field)) { - return $this->translate($field, $locale, false); - } - - - // fallback to parent - return parent::getAttribute($key); - } - public function clearTranslations(?string $locale = null): void { // clear all translations if none is provided diff --git a/tests/TestSupport/Factories/PostFactory.php b/tests/TestSupport/Factories/PostFactory.php new file mode 100644 index 0000000..c8f67e6 --- /dev/null +++ b/tests/TestSupport/Factories/PostFactory.php @@ -0,0 +1,33 @@ + $this->faker->title(), + 'slug' => $this->faker->slug(), + 'body' => $this->faker->paragraph(5), + 'lang' => $this->faker->languageCode(), + ]; + } + + public function withAuthor(): self + { + return $this->state(function (array $attributes) { + return [ + 'author_id' => Author::factory(), + ]; + }); + } +} diff --git a/tests/TestSupport/Models/Article.php b/tests/TestSupport/Models/Article.php index a14831e..26e424d 100644 --- a/tests/TestSupport/Models/Article.php +++ b/tests/TestSupport/Models/Article.php @@ -42,9 +42,4 @@ public function getNonTranslatablePivots(): array 'author_id' ]; } - - public function getDefaultTranslationLocale(): string - { - // TODO: Implement getDefaultTranslationLocale() method. - } } diff --git a/tests/TestSupport/Models/Post.php b/tests/TestSupport/Models/Post.php new file mode 100644 index 0000000..672d3ae --- /dev/null +++ b/tests/TestSupport/Models/Post.php @@ -0,0 +1,43 @@ +belongsTo(Author::class); + } + + public function getTranslatables(): array + { + return [ + 'title', + 'body' + ]; + } + + public function getNonTranslatablePivots(): array + { + return [ + 'author_id' + ]; + } +} diff --git a/tests/TestSupport/database/2025_02_20_061338_create_posts_table.php b/tests/TestSupport/database/2025_02_20_061338_create_posts_table.php new file mode 100644 index 0000000..9e2636a --- /dev/null +++ b/tests/TestSupport/database/2025_02_20_061338_create_posts_table.php @@ -0,0 +1,31 @@ +id(); + + $table->string('title'); + $table->string('slug'); + $table->text('body'); + $table->foreignIdFor(Author::class); + + DbTranslatableSchema::columns($table); + + $table->timestamps(); + $table->softDeletes(); + }); + } + + public function down(): void + { + Schema::dropIfExists('posts'); + } +}; diff --git a/tests/Unit/DbTranslatable/IsDbTranslatableTest.php b/tests/Unit/DbTranslatable/IsDbTranslatableTest.php new file mode 100644 index 0000000..e6b5ac2 --- /dev/null +++ b/tests/Unit/DbTranslatable/IsDbTranslatableTest.php @@ -0,0 +1,326 @@ +assertEquals([ + 'id', + 'translatable_parent_id', + 'lang', + 'created_at', + 'updated_at', + 'deleted_at', + ], $post->getFieldsIgnoredForTranslation()); + } + + /** @test */ + public function it_can_get_translatable_fields() + { + $post = new Post(); + + $this->assertEquals([ + 'title', + 'body', + ], $post->getTranslatables()); + } + + /** @test */ + public function it_can_get_non_translatable_fields() + { + $post = new Post(); + + $this->assertEquals([ + 'slug', + ], $post->getNonTranslatables()); + } + + /** @test */ + public function it_can_get_non_translatable_pivots() + { + $post = new Post(); + + $this->assertEquals([ + 'author_id', + ], $post->getNonTranslatablePivots()); + } + + /** @test */ + public function it_can_get_all_non_translatable_fields() + { + $post = new Post(); + + $this->assertEquals([ + 'slug', + 'id', + 'translatable_parent_id', + 'lang', + 'created_at', + 'updated_at', + 'deleted_at', + 'author_id', + ], $post->getAllNonTranslatables()); + } + + /** @test */ + public function it_can_check_if_is_a_non_translatable_pivot() + { + $post = new Post(); + + $this->assertFalse($post->isNonTranslatablePivot('id')); + $this->assertTrue($post->isNonTranslatablePivot('author_id')); + } + + /** @test */ + public function it_can_get_the_default_translation_locale() + { + $post = Post::factory()->withAuthor()->create([ + 'lang' => 'en', + ]); + + $this->assertFalse($post->isDefaultTranslationLocale('fr')); + $this->assertTrue($post->isDefaultTranslationLocale('en')); + } + + /** @test */ + public function it_can_get_allowed_translation_locales() + { + $post = Post::factory()->withAuthor()->create(); + + $this->assertEquals(['en', 'dv', 'jp'], $post->getAllowedTranslationLocales()); + } + + /** @test */ + public function it_can_check_if_given_locale_is_allowed() + { + $post = Post::factory()->withAuthor()->create(); + + $this->assertTrue($post->isAllowedTranslationLocale('en')); + $this->assertTrue($post->isAllowedTranslationLocale('dv')); + $this->assertFalse($post->isAllowedTranslationLocale('fr')); + } + + /** @test */ + public function it_can_translate_field_via_translate_function() + { + $post = Post::factory()->withAuthor()->create([ + 'lang' => 'en', + 'title' => 'This is an English title', + 'slug' => 'this-is-an-english-slug', + 'body' => 'This is an English body', + ]); + $post_dv = Post::factory()->withAuthor()->create([ + 'lang' => 'dv', + 'title' => 'Mee dhivehi title eh', + 'slug' => 'mee-dhivehi-slug-eh', + 'body' => 'Mee dhivehi liyumeh', + ]); + $post_jp = Post::factory()->withAuthor()->create([ + 'lang' => 'jp', + 'title' => 'Kore wa taitorudesu', + 'slug' => 'kore-wa-namekujidesu', + 'body' => 'Kore wa kijidesu', + ]); + + $this->assertEquals('Mee dhivehi title eh', $post->translate('title', 'dv')); + $this->assertEquals('Mee dhivehi liyumeh', $post->translate('body', 'dv')); + + $this->assertEquals('Kore wa taitorudesu', $post->translate('title', 'jp')); + $this->assertEquals('Kore wa kijidesu', $post->translate('body', 'jp')); + } + + /** @test */ + public function it_can_translate_field_via_translate_function_without_fallback() + { + $post = Post::factory()->withAuthor()->create([ + 'lang' => 'en', + 'title' => 'This is an English title', + 'slug' => 'this-is-an-english-slug', + 'body' => 'This is an English body', + ]); + $post_dv = Post::factory()->withAuthor()->create([ + 'lang' => 'dv', + 'title' => 'Mee dhivehi title eh', + 'slug' => 'mee-dhivehi-slug-eh', + 'body' => 'Mee dhivehi liyumeh', + ]); + + $this->assertEquals('This is an English title', $post->translate('title', 'jp')); + $this->assertEquals('this-is-an-english-slug', $post->translate('slug', 'jp')); + $this->assertEquals('This is an English body', $post->translate('body', 'jp')); + + $this->assertNull($post->translate('title', 'jp', false)); + $this->assertNull($post->translate('slug', 'jp', false)); + $this->assertNull($post->translate('body', 'jp', false)); + } + + /** @test */ + public function it_can_translate_field_via_magic_method() + { + $post = Post::factory()->withAuthor()->create(); + $post_dv = Post::factory()->withAuthor()->create([ + 'lang' => 'dv', + 'title' => 'Mee dhivehi title eh', + 'slug' => 'mee-dhivehi-slug-eh', + 'body' => 'Mee dhivehi liyumeh', + ]); + $post_jp = Post::factory()->withAuthor()->create([ + 'lang' => 'jp', + 'title' => 'Kore wa taitorudesu', + 'slug' => 'kore-wa-namekujidesu', + 'body' => 'Kore wa kijidesu', + ]); + + $this->assertEquals('Mee dhivehi title eh', $post->title_dv); + $this->assertEquals('Mee dhivehi liyumeh', $post->body_dv); + + // slugs should not be translated + $this->assertNull($post->slug_dv); + } + + /** @test */ + public function it_can_translate_field_via_locale_change() + { + $post = Post::factory()->withAuthor()->create(); + $post_dv = Post::factory()->withAuthor()->create([ + 'lang' => 'dv', + 'title' => 'Mee dhivehi title eh', + 'slug' => 'mee-dhivehi-slug-eh', + 'body' => 'Mee dhivehi liyumeh', + ]); + $post_jp = Post::factory()->withAuthor()->create([ + 'lang' => 'jp', + 'title' => 'Kore wa taitorudesu', + 'slug' => 'kore-wa-namekujidesu', + 'body' => 'Kore wa kijidesu', + ]); + + $tmp = app()->getLocale(); + app()->setLocale('dv'); + $this->assertEquals('Mee dhivehi title eh', $post->title); + $this->assertEquals('Mee dhivehi liyumeh', $post->body); + app()->setLocale($tmp); + } + + /** @test */ + public function it_can_check_if_given_field_is_translatable() + { + $post = Post::factory()->withAuthor()->create(); + $post_dv = Post::factory()->withAuthor()->create([ + 'lang' => 'dv', + 'title' => 'Mee dhivehi title eh', + 'slug' => 'mee-dhivehi-slug-eh', + 'body' => 'Mee dhivehi liyumeh', + ]); + $post_jp = Post::factory()->withAuthor()->create([ + 'lang' => 'jp', + 'title' => 'Kore wa taitorudesu', + 'slug' => 'kore-wa-namekujidesu', + 'body' => 'Kore wa kijidesu', + ]); + + $this->assertTrue($post->isTranslatable('title')); + } + + /** @test */ + public function it_can_clear_translations_for_one_locale() + { + $post = Post::factory()->withAuthor()->create(); + $post_dv = Post::factory()->withAuthor()->create([ + 'lang' => 'dv', + 'title' => 'Mee dhivehi title eh', + 'slug' => 'mee-dhivehi-slug-eh', + 'body' => 'Mee dhivehi liyumeh', + ]); + $post_jp = Post::factory()->withAuthor()->create([ + 'lang' => 'jp', + 'title' => 'Kore wa taitorudesu', + 'slug' => 'kore-wa-namekujidesu', + 'body' => 'Kore wa kijidesu', + ]); + + $post->clearTranslations('dv'); + + // Ensure that dv is gone while jp is still there + $this->assertEmpty($post->title_dv); + $this->assertEquals('Kore wa taitorudesu', $post->title_jp); + } + + /** @test */ + public function it_can_clear_translations_for_all_locales() + { + $post = Post::factory()->withAuthor()->create(); + $post_dv = Post::factory()->withAuthor()->create([ + 'lang' => 'dv', + 'title' => 'Mee dhivehi title eh', + 'slug' => 'mee-dhivehi-slug-eh', + 'body' => 'Mee dhivehi liyumeh', + ]); + $post_jp = Post::factory()->withAuthor()->create([ + 'lang' => 'jp', + 'title' => 'Kore wa taitorudesu', + 'slug' => 'kore-wa-namekujidesu', + 'body' => 'Kore wa kijidesu', + ]); + + $post->clearTranslations(); + + $this->assertEmpty($post->title_dv); + $this->assertEmpty($post->title_jp); + } + + /** @test */ + public function it_can_check_if_any_translation_for_a_specific_locale() + { + $post = Post::factory()->withAuthor()->create(); + $post_dv = Post::factory()->withAuthor()->create([ + 'lang' => 'dv', + 'title' => 'Mee dhivehi title eh', + 'slug' => 'mee-dhivehi-slug-eh', + 'body' => 'Mee dhivehi liyumeh', + ]); + $post_jp = Post::factory()->withAuthor()->create([ + 'lang' => 'jp', + 'title' => 'Kore wa taitorudesu', + 'slug' => 'kore-wa-namekujidesu', + 'body' => 'Kore wa kijidesu', + ]); + + $this->assertFalse($post->hasTranslation('fr')); + $this->assertTrue($post->hasTranslation('dv')); + } + + /** @test */ + public function it_can_check_if_is_default_translation_locale() + { + $post = Post::factory()->withAuthor()->create([ + 'lang' => 'en', + ]); + $post_dv = Post::factory()->withAuthor()->create([ + 'lang' => 'dv', + 'title' => 'Mee dhivehi title eh', + 'slug' => 'mee-dhivehi-slug-eh', + 'body' => 'Mee dhivehi liyumeh', + ]); + $post_jp = Post::factory()->withAuthor()->create([ + 'lang' => 'jp', + 'title' => 'Kore wa taitorudesu', + 'slug' => 'kore-wa-namekujidesu', + 'body' => 'Kore wa kijidesu', + ]); + + $this->assertFalse($post->isDefaultTranslationLocale('fr')); + $this->assertTrue($post->isDefaultTranslationLocale('en')); + } +} From 1a60199a8017493dd7933b5775e8ebab8d6d77f7 Mon Sep 17 00:00:00 2001 From: WovenCoast Date: Thu, 20 Feb 2025 13:04:00 +0500 Subject: [PATCH 04/24] fix: factories faker compatibility and unit tests syntax error --- .github/workflows/run-tests.yml | 2 +- tests/TestSupport/Factories/ArticleFactory.php | 5 ++++- tests/TestSupport/Factories/AuthorFactory.php | 2 +- tests/TestSupport/Factories/PostFactory.php | 11 ++++++----- 4 files changed, 12 insertions(+), 8 deletions(-) diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index fff207c..0072a0f 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -50,7 +50,7 @@ jobs: with: php-version: ${{ matrix.php }} extensions: curl, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, iconv - coverage: none\ + coverage: none - name: Install dependencies run: | diff --git a/tests/TestSupport/Factories/ArticleFactory.php b/tests/TestSupport/Factories/ArticleFactory.php index c245776..d9ea0b3 100644 --- a/tests/TestSupport/Factories/ArticleFactory.php +++ b/tests/TestSupport/Factories/ArticleFactory.php @@ -3,6 +3,7 @@ namespace Javaabu\Translatable\Tests\TestSupport\Factories; use Illuminate\Database\Eloquent\Factories\Factory; +use Illuminate\Support\Carbon; use Javaabu\Translatable\Tests\TestSupport\Models\Article; use Javaabu\Translatable\Tests\TestSupport\Models\Author; @@ -16,7 +17,9 @@ public function definition(): array 'title' => fake()->title(), 'slug' => fake()->slug(), 'body' => fake()->paragraph(5), - 'lang' => fake()->locale() + 'lang' => fake()->locale(), + 'created_at' => Carbon::now(), + 'updated_at' => Carbon::now(), ]; } diff --git a/tests/TestSupport/Factories/AuthorFactory.php b/tests/TestSupport/Factories/AuthorFactory.php index a6b9c69..ae21013 100644 --- a/tests/TestSupport/Factories/AuthorFactory.php +++ b/tests/TestSupport/Factories/AuthorFactory.php @@ -13,7 +13,7 @@ class AuthorFactory extends Factory public function definition(): array { return [ - 'name' => $this->faker->name(), + 'name' => fake()->name(), 'created_at' => Carbon::now(), 'updated_at' => Carbon::now(), ]; diff --git a/tests/TestSupport/Factories/PostFactory.php b/tests/TestSupport/Factories/PostFactory.php index c8f67e6..ed01db8 100644 --- a/tests/TestSupport/Factories/PostFactory.php +++ b/tests/TestSupport/Factories/PostFactory.php @@ -4,7 +4,6 @@ use Illuminate\Database\Eloquent\Factories\Factory; use Illuminate\Support\Carbon; -use Javaabu\Translatable\Tests\TestSupport\Models\Article; use Javaabu\Translatable\Tests\TestSupport\Models\Author; use Javaabu\Translatable\Tests\TestSupport\Models\Post; @@ -15,10 +14,12 @@ class PostFactory extends Factory public function definition(): array { return [ - 'title' => $this->faker->title(), - 'slug' => $this->faker->slug(), - 'body' => $this->faker->paragraph(5), - 'lang' => $this->faker->languageCode(), + 'title' => fake()->title(), + 'slug' => fake()->slug(), + 'body' => fake()->paragraph(5), + 'lang' => fake()->locale(), + 'created_at' => Carbon::now(), + 'updated_at' => Carbon::now(), ]; } From 12795f18a5744519d9b3230f35bf44b938ca2686 Mon Sep 17 00:00:00 2001 From: WovenCoast Date: Thu, 20 Feb 2025 16:26:55 +0500 Subject: [PATCH 05/24] test: support for laravel 9 --- phpunit.xml.dist | 5 ++++ .../TestSupport/Factories/ArticleFactory.php | 8 +++---- tests/TestSupport/Factories/AuthorFactory.php | 2 +- tests/TestSupport/Factories/PostFactory.php | 8 +++---- .../IsJsonTranslatableTest.php | 23 +++++++++++++++++++ 5 files changed, 37 insertions(+), 9 deletions(-) diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 899b1ed..edef407 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -10,6 +10,11 @@ tests + + + src + + diff --git a/tests/TestSupport/Factories/ArticleFactory.php b/tests/TestSupport/Factories/ArticleFactory.php index d9ea0b3..a4d7832 100644 --- a/tests/TestSupport/Factories/ArticleFactory.php +++ b/tests/TestSupport/Factories/ArticleFactory.php @@ -14,10 +14,10 @@ class ArticleFactory extends Factory public function definition(): array { return [ - 'title' => fake()->title(), - 'slug' => fake()->slug(), - 'body' => fake()->paragraph(5), - 'lang' => fake()->locale(), + 'title' => $this->faker->title(), + 'slug' => $this->faker->slug(), + 'body' => $this->faker->paragraph(5), + 'lang' => $this->faker->locale(), 'created_at' => Carbon::now(), 'updated_at' => Carbon::now(), ]; diff --git a/tests/TestSupport/Factories/AuthorFactory.php b/tests/TestSupport/Factories/AuthorFactory.php index ae21013..a6b9c69 100644 --- a/tests/TestSupport/Factories/AuthorFactory.php +++ b/tests/TestSupport/Factories/AuthorFactory.php @@ -13,7 +13,7 @@ class AuthorFactory extends Factory public function definition(): array { return [ - 'name' => fake()->name(), + 'name' => $this->faker->name(), 'created_at' => Carbon::now(), 'updated_at' => Carbon::now(), ]; diff --git a/tests/TestSupport/Factories/PostFactory.php b/tests/TestSupport/Factories/PostFactory.php index ed01db8..df4c021 100644 --- a/tests/TestSupport/Factories/PostFactory.php +++ b/tests/TestSupport/Factories/PostFactory.php @@ -14,10 +14,10 @@ class PostFactory extends Factory public function definition(): array { return [ - 'title' => fake()->title(), - 'slug' => fake()->slug(), - 'body' => fake()->paragraph(5), - 'lang' => fake()->locale(), + 'title' => $this->faker->title(), + 'slug' => $this->faker->slug(), + 'body' => $this->faker->paragraph(5), + 'lang' => $this->faker->locale(), 'created_at' => Carbon::now(), 'updated_at' => Carbon::now(), ]; diff --git a/tests/Unit/JsonTranslatable/IsJsonTranslatableTest.php b/tests/Unit/JsonTranslatable/IsJsonTranslatableTest.php index 86e249b..763bf01 100644 --- a/tests/Unit/JsonTranslatable/IsJsonTranslatableTest.php +++ b/tests/Unit/JsonTranslatable/IsJsonTranslatableTest.php @@ -106,9 +106,25 @@ public function it_can_translate_field_via_translate_function() $this->assertEquals('Mee dhivehi title eh', $article->translate('title', 'dv')); $this->assertEquals('Mee dhivehi liyumeh', $article->translate('body', 'dv')); + $this->assertNull($article->translate('slug', 'dv', false)); + + $tmp = app()->getLocale(); + app()->setLocale('en'); + $this->assertEquals('This is an English title', $article->translate('title')); + $this->assertEquals('This is an English body', $article->translate('body')); + $this->assertEquals('this-is-an-english-slug', $article->translate('slug')); + + app()->setLocale('dv'); + $this->assertEquals('Mee dhivehi title eh', $article->translate('title')); + $this->assertEquals('Mee dhivehi liyumeh', $article->translate('body')); + $this->assertNull($article->translate('slug', fallback: false)); + app()->setLocale($tmp); $this->assertEquals('Kore wa taitorudesu', $article->translate('title', 'jp')); $this->assertEquals('Kore wa kijidesu', $article->translate('body', 'jp')); + $this->assertNull($article->translate('slug', 'jp', false)); + + $this->assertNull($article->translate('slug', 'fr', false)); } /** @test */ @@ -226,6 +242,9 @@ public function it_can_clear_translations_for_one_locale() ] ]); + // check if it can clear language that doesn't exist + $article->clearTranslations('fr'); + $article->clearTranslations('dv'); // Ensure that dv is gone while jp is still there @@ -277,6 +296,10 @@ public function it_can_check_if_any_translation_for_a_specific_locale() $this->assertFalse($article->hasTranslation('fr')); $this->assertTrue($article->hasTranslation('dv')); + $tmp = app()->getLocale(); + app()->setLocale('dv'); + $this->assertTrue($article->hasTranslation()); + app()->setLocale($tmp); } /** @test */ From d64415bbdfcab7988c2e803d595983c13c9bb948 Mon Sep 17 00:00:00 2001 From: WovenCoast Date: Thu, 20 Feb 2025 16:34:45 +0500 Subject: [PATCH 06/24] coverage: run --- .editorconfig | 3 + .github/workflows/run-tests.yml | 121 +++++++++++++++++--------------- 2 files changed, 68 insertions(+), 56 deletions(-) diff --git a/.editorconfig b/.editorconfig index cd8eb86..df8e24c 100644 --- a/.editorconfig +++ b/.editorconfig @@ -13,3 +13,6 @@ trim_trailing_whitespace = true [*.md] trim_trailing_whitespace = false + +[*.yml] +indent_size = 2 \ No newline at end of file diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index 0072a0f..cb72e68 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -1,61 +1,70 @@ name: tests on: - pull_request: - branches: - - main - push: - branches: - - main - - dev + pull_request: + branches: + - main + push: + branches: + - main + - dev jobs: - tests: - runs-on: ${{ matrix.os }} - - strategy: - fail-fast: true - matrix: - os: [ ubuntu-latest ] - php: [ 8.3, 8.2, 8.1 ] - laravel: [ 11.*, 10.*, 9.* ] - stability: [ prefer-lowest, prefer-stable ] - include: - - laravel: 11.* - testbench: 9.* - carbon: ^2.63 - - laravel: 10.* - testbench: 8.* - carbon: ^2.63 - - laravel: 9.* - testbench: 7.* - carbon: ^2.63 - exclude: - - laravel: 11.* - php: 8.1 - - name: P${{ matrix.php }} - L${{ matrix.laravel }} - ${{ matrix.stability }} - ${{ matrix.os }} - - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Install SQLite 3 - run: | - sudo apt-get update - sudo apt-get install sqlite3 -y - - - name: Setup PHP - uses: shivammathur/setup-php@v2 - with: - php-version: ${{ matrix.php }} - extensions: curl, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, iconv - coverage: none - - - name: Install dependencies - run: | - composer require "laravel/framework:${{ matrix.laravel }}" "orchestra/testbench:${{ matrix.testbench }}" "nesbot/carbon:${{ matrix.carbon }}" --no-interaction --no-update - composer update --${{ matrix.stability }} --prefer-dist --no-interaction - - - name: Execute tests - run: vendor/bin/phpunit + tests: + runs-on: ${{ matrix.os }} + + strategy: + fail-fast: true + matrix: + os: [ ubuntu-latest ] + php: [ 8.3, 8.2, 8.1 ] + laravel: [ 11.*, 10.*, 9.* ] + stability: [ prefer-lowest, prefer-stable ] + include: + - laravel: 11.* + testbench: 9.* + carbon: ^2.63 + - laravel: 10.* + testbench: 8.* + carbon: ^2.63 + - laravel: 9.* + testbench: 7.* + carbon: ^2.63 + exclude: + - laravel: 11.* + php: 8.1 + + name: P${{ matrix.php }} - L${{ matrix.laravel }} - ${{ matrix.stability }} - ${{ matrix.os }} + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Install SQLite 3 + run: | + sudo apt-get update + sudo apt-get install sqlite3 -y + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + extensions: curl, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, iconv + coverage: ${{ startsWith(github.event.head_commit.message, 'coverage') && 'xdebug' || 'none' }} + + - name: Install dependencies + run: | + composer require "laravel/framework:${{ matrix.laravel }}" "orchestra/testbench:${{ matrix.testbench }}" "nesbot/carbon:${{ matrix.carbon }}" --no-interaction --no-update + composer update --${{ matrix.stability }} --prefer-dist --no-interaction + + - name: Execute tests + run: | + vendor/bin/phpunit ${{ startsWith(github.event.head_commit.message, 'coverage') && '--coverage-clover=clover.xml' || '' }} + + - name: Make code coverage badge + if: startsWith(github.event.head_commit.message, 'coverage') + uses: timkrase/phpunit-coverage-badge@v1.2.1 + with: + coverage_badge_path: .github/coverage.svg + push_badge: true + repo_token: ${{ secrets.GITHUB_TOKEN }} From 9532d277f3b5d995e2f94b1b6b3d02bfa549264b Mon Sep 17 00:00:00 2001 From: Github Actions Bot <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 20 Feb 2025 11:35:31 +0000 Subject: [PATCH 07/24] Update code coverage badge --- .github/coverage.svg | 20 ++ clover.xml | 525 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 545 insertions(+) create mode 100644 .github/coverage.svg create mode 100644 clover.xml diff --git a/.github/coverage.svg b/.github/coverage.svg new file mode 100644 index 0000000..e47662c --- /dev/null +++ b/.github/coverage.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + coverage + + 26 % + + \ No newline at end of file diff --git a/clover.xml b/clover.xml new file mode 100644 index 0000000..45196f1 --- /dev/null +++ b/clover.xml @@ -0,0 +1,525 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From 70ee66a9d2659a6a75d8195938047393003e314e Mon Sep 17 00:00:00 2001 From: WovenCoast Date: Thu, 20 Feb 2025 16:46:34 +0500 Subject: [PATCH 08/24] coverage: fix --- .github/workflows/run-tests.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index cb72e68..2860d21 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -50,7 +50,7 @@ jobs: with: php-version: ${{ matrix.php }} extensions: curl, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, iconv - coverage: ${{ startsWith(github.event.head_commit.message, 'coverage') && 'xdebug' || 'none' }} + coverage: ${{ startsWith(github.event.head_commit.message, 'coverage') && matrix.php == '8.3' && matrix.laravel == '11.*' && matrix.stability == 'prefer-stable' && 'xdebug' || 'none' }} - name: Install dependencies run: | @@ -59,10 +59,10 @@ jobs: - name: Execute tests run: | - vendor/bin/phpunit ${{ startsWith(github.event.head_commit.message, 'coverage') && '--coverage-clover=clover.xml' || '' }} + vendor/bin/phpunit ${{ startsWith(github.event.head_commit.message, 'coverage') && matrix.php == '8.3' && matrix.laravel == '11.*' && matrix.stability == 'prefer-stable' && '--coverage-clover=clover.xml' || '' }} - name: Make code coverage badge - if: startsWith(github.event.head_commit.message, 'coverage') + if: startsWith(github.event.head_commit.message, 'coverage') && matrix.php == '8.3' && matrix.laravel == '11.*' && matrix.stability == 'prefer-stable' uses: timkrase/phpunit-coverage-badge@v1.2.1 with: coverage_badge_path: .github/coverage.svg From 6c8c41abad664154a9e1181854ac2bdd2666539b Mon Sep 17 00:00:00 2001 From: WovenCoast Date: Sun, 23 Feb 2025 10:31:30 +0500 Subject: [PATCH 09/24] coverage: remove unneccessary files --- src/DbTranslatable/IsDbTranslatable.php | 29 +- src/V1/Abstract/IsTranslatable.php | 126 ---- src/V1/Enums/LanguagesOld.php | 122 ---- .../JsonTranslatable/IsJsonTranslatable.php | 137 ---- .../IsJsonTranslatableOld.php | 657 ------------------ .../DbTranslatable/IsDbTranslatableTest.php | 52 +- .../IsJsonTranslatableTest.php | 58 +- 7 files changed, 98 insertions(+), 1083 deletions(-) delete mode 100644 src/V1/Abstract/IsTranslatable.php delete mode 100644 src/V1/Enums/LanguagesOld.php delete mode 100644 src/V1/JsonTranslatable/IsJsonTranslatable.php delete mode 100644 src/V1/JsonTranslatable/IsJsonTranslatableOld.php diff --git a/src/DbTranslatable/IsDbTranslatable.php b/src/DbTranslatable/IsDbTranslatable.php index 2bde38b..9e6d0e8 100644 --- a/src/DbTranslatable/IsDbTranslatable.php +++ b/src/DbTranslatable/IsDbTranslatable.php @@ -18,17 +18,19 @@ public function getFieldsIgnoredForTranslation(): array ))); } - public function translations() { + public function translations() + { return $this->hasMany($this, 'translatable_parent_id'); } - public function defaultTranslation() { + public function defaultTranslation() + { return $this->belongsTo($this, 'translatable_parent_id'); } public function isDefaultTranslation(): bool { - return $this->getAttributeValue('translatable_parent_id') !== null; + return $this->getAttributeValue('translatable_parent_id') != null; } public function translate(string $field, ?string $locale = null, bool $fallback = true): mixed @@ -44,31 +46,33 @@ public function translate(string $field, ?string $locale = null, bool $fallback } // If the locale is not allowed then return null - if (! $this->isAllowedTranslationLocale($locale)) { + if (!$this->isAllowedTranslationLocale($locale)) { return $fallback ? $this->getAttributeValue($field) : null; } // If the field is not in the translatable fields list then return null - if (! $this->isTranslatable($field)) { + if (!$this->isTranslatable($field)) { return $fallback ? $this->getAttributeValue($field) : null; } if ($this->isDefaultTranslation()) { // if there's no parent, this is the main one. get translations using the defined relation $translation = $this->translations()->where('lang', $locale)->first(); + dd($translation); } else { // otherwise it's already a translation, get all translations including the parent $translation = self::query()->where([ - 'id' => $this->translatable_parent_id, 'lang' => $locale, + 'id' => $this->translatable_parent_id, ])->orWhere([ - 'parent_id' => $this->translatable_parent_id, 'lang' => $locale, + 'parent_id' => $this->translatable_parent_id, ])->first(); } + // fallback if the translation doesn't exist - if (! $translation) { + if (!$translation) { return $fallback ? $this->getAttributeValue($field) : null; } @@ -88,15 +92,18 @@ public function clearTranslations(?string $locale = null): void if (is_null($locale)) { // nuke all except the main one $parent_id = $this->isDefaultTranslation() ? $this->id : $this->translatable_parent_id; - self::query()->where('translatable_parent_id', $parent_id)->forceDelete(); + self::query()->where('translatable_parent_id', $parent_id)->withTrashed()->forceDelete(); } else { // check the current one lang, if it's correct delete it if ($this->lang == $locale) { - $this->forceDelete(); + if (!$this->isDefaultTranslation()) { + $this->forceDelete(); + } } else { if ($this->isDefaultTranslation()) { $translation = $this->translations()->where('lang', $locale); - $translation->forceDelete(); + // deleting the default translation will soft lock + $translation->delete(); } else { $translation = self::query()->where([ 'translatable_parent_id' => $this->translatable_parent_id, diff --git a/src/V1/Abstract/IsTranslatable.php b/src/V1/Abstract/IsTranslatable.php deleted file mode 100644 index c56a844..0000000 --- a/src/V1/Abstract/IsTranslatable.php +++ /dev/null @@ -1,126 +0,0 @@ -fallback_translations = config('translatable.fallback_translations') == 'true'; - } - - /** - * Check whether to show translation fallbacks - * - * @return boolean - */ - public function shouldFallbackForTranslations(): bool - { - return $this->fallback_translations; - } - - /** - * Set whether to show translation fallbacks - */ - public function setShouldFallbackForTranslations(bool $fallback_translations): self - { - $this->fallback_translations = $fallback_translations; - return $this; - } - - /** - * Get the translation ignored fields - * - * @return array - */ - public function getFieldsIgnoredForTranslation(): array - { - return config('translatable.fields_ignored_for_translation'); - } - - /** - * Check is default translation locale - * - * @param string $locale - * @return boolean - */ - public function isDefaultTranslationLocale(string $locale): bool - { - return strtolower($this->getDefaultTranslationLocale()) == strtolower($locale); - } - - /** - * Get default translation locale - * - * @return string - */ - public function getDefaultTranslationLocale(): string - { - return app()->getLocale(); - } - - /** - * Get allowed translation locales - * - * @return array - */ - public function getAllowedTranslationLocales(): array - { - return array_keys(config('translatable.allowed_translation_locales')); - } - - /** - * Check if given locale is allowed - * - * @param string $locale - * @return boolean - */ - public function isAllowedTranslationLocale(string $locale): bool - { - return in_array($locale, $this->getAllowedTranslationLocales()); - } - - /** - * Get all pivots that must not be translatable - * - * @return array - */ - abstract public function getNonTranslatablePivots(): array; - - public function getNonTranslatables() { - $all_fields = \Schema::getColumnListing($this->getTable()); - - $hide = array_merge($this->getTranslatables(), $this->getFieldsIgnoredForTranslation()); - - return array_values(array_diff($all_fields, $hide)); - } - - /** - * Get all pivots and attributes that must not be translatable - * - * @return array - */ - public function getAllNonTranslatables(): array - { - return array_merge( - $this->getNonTranslatablePivots(), - $this->getNonTranslatables() - ); - } - - /** - * Check if is a non translatable pivot - * - * @param string $relation - * @return boolean - */ - public function isNonTranslatablePivot(string $relation): bool - { - return in_array($relation, $this->getNonTranslatablePivots()); - } -} diff --git a/src/V1/Enums/LanguagesOld.php b/src/V1/Enums/LanguagesOld.php deleted file mode 100644 index 6f0530f..0000000 --- a/src/V1/Enums/LanguagesOld.php +++ /dev/null @@ -1,122 +0,0 @@ - __('Dhivehi'), - static::EN => __('English'), - ]; - } - - protected static function getFlag($key): string - { - return static::$flags[$key] ?? ''; - } - - public static function flagUrl($key): string - { - return Flags::getFlagUrl(self::getFlag($key)); - } - - public static function getLocaleFlag($current_locale = null, $opposite = false): string - { - if (!$current_locale) { - $current_locale = app()->getLocale(); - } - - $locale = $opposite ? self::getOppositeLocale() : $current_locale; - - return self::flagUrl($locale); - } - - public static function getOppositeLocaleFlag($current_locale = null): string - { - return self::getLocaleFlag($current_locale, true); - } - - public static function getDefaultTranslationLocale(): string - { - return config('translations.default_translation_locale'); - } - - public static function isRtl($value): bool - { - return $value == self::DV; - } - - public static function getDirection($current_locale = null): string - { - if (!$current_locale) { - $current_locale = app()->getLocale(); - } - - return self::isRtl($current_locale) ? 'rtl' : 'ltr'; - } - - public static function getOppositeLocale($currentLocale = null): string - { - if (!$currentLocale) { - $currentLocale = app()->getLocale(); - } - - return $currentLocale == self::DV ? self::EN : self::DV; - } - - public static function translateCurrentRoute(): string - { - $current_route = Route::getCurrentRoute()->getName(); - - $route_params = Route::getCurrentRoute()->parameters(); - - $switch_to = self::getOppositeLocale(); - - $route_params['language'] = $switch_to; - - return route($current_route, $route_params); - } - - public static function getLocalizedUrl($translatable, $locale = null): string - { - if (!$locale) { - $locale = self::getOppositeLocale(); - } - - $url = null; - - if ($translatable instanceof Translatable) { - $url = $translatable->getLocalizedUrl($locale); - } elseif ($translatable) { - $url = public_url('/' . $locale . '/' . ltrim($translatable, '/')); - } - - return $url ?: public_url('/' . $locale); - } - - /** - * Set current session locale - * - * @return string - */ - public static function getSessionLocale(): string - { - return session()->get('language', static::getDefaultTranslationLocale()); - } -} diff --git a/src/V1/JsonTranslatable/IsJsonTranslatable.php b/src/V1/JsonTranslatable/IsJsonTranslatable.php deleted file mode 100644 index 3a80948..0000000 --- a/src/V1/JsonTranslatable/IsJsonTranslatable.php +++ /dev/null @@ -1,137 +0,0 @@ -getTable()); - - $hide = array_merge($this->getTranslatables(), $this->getFieldsIgnoredForTranslation()); - - return array_values(array_diff($all_fields, $hide)); - } - - public function translate(string $field, ?string $locale = null, bool $fallback = true): mixed - { - if (! $locale && $fallback) { - $locale = app()->getLocale(); - } - - if (! $locale || ! $this->isAllowedTranslationLocale($locale)) { - return null; - } - - if ($this->isPrimaryLocale($locale)) { - // for default locale just use the direct field value - $value = parent::getAttribute($field); - } else { - // for other locales, use the appropriate translation for that locale - $value = $this->translations[$locale][$field] ?? null; - - if ($value && $this->hasCast($field) && is_string($value)) { - $value = $this->castAttribute($field, $value); - } - - // fallback to default if translation missing - if (empty($value) && $fallback) { - $value = parent::getAttribute($field); - } - } - - return $value; - } - - public function isPrimaryLocale(string $locale): bool - { - return $locale == $this->getAttribute('lang'); - } - - public function isTranslatable(string $field): bool - { - return in_array($field, $this->getTranslatables()); - } - - public function clearTranslations(?string $locale = null): void - { - $this->translation = null; - } - - public function hasTranslation(?string $locale = null): bool - { - if (is_null($locale)) { - $locale = app()->getLocale(); - } - - // if the row is already in the correct language - if ($this->isPrimaryLocale($locale)) { - return true; - } - - // if the provided locale is not allowed we can ignore - if ($this->isAllowedTranslationLocale($locale)) { - return false; - } - - // check if translation exists for locale - $values = $this->translations[$locale]; - return ! empty($values); - } - - - public function getFieldAndLocale(string $key) - { - $locale = Str::afterLast($key, '_'); - - if (empty($locale) || (! $this->isAllowedTranslationLocale($locale))) { - return [$key, null]; - } - - $field = Str::beforeLast($locale, '_'); - return [$key, $field]; - } - - /** - * Get an attribute from the model. - * - * @param string $key - * - * @return mixed - */ - public function getAttribute($key): mixed - { - // Add support for compoships - if (is_array($key)) { //Check for multi-columns relationship - return array_map(function ($k) { - return parent::getAttribute($k); - }, $key); - } - - - // translate if translatable - if ($this->isTranslatable($key)) { - return $this->translate($key, null, $this->shouldFallbackForTranslations()); - } - - // check if is a suffixed attribute - [$field, $locale] = $this->getFieldAndLocale($key); - - if ($locale && $this->isTranslatable($field)) { - return $this->translate($field, $locale, false); - } - - // fallback to parent - if (($attr = parent::getAttribute($key)) !== null) { - return $attr; - } - - return null; - } -} diff --git a/src/V1/JsonTranslatable/IsJsonTranslatableOld.php b/src/V1/JsonTranslatable/IsJsonTranslatableOld.php deleted file mode 100644 index c174698..0000000 --- a/src/V1/JsonTranslatable/IsJsonTranslatableOld.php +++ /dev/null @@ -1,657 +0,0 @@ -lang) { - $model->lang = app()->getLocale(); - } - }); - } - - /** - * Check whether to show translation fallbacks - * - * @return boolean - */ - public function shouldFallbackForTranslations(): bool - { - return $this->fallback_translations; - } - - /** - * Set to show translation fallbacks - * - * @return void - */ - public function showTranslationFallbacks(): void - { - $this->fallback_translations = true; - } - - /** - * Set to not show translation fallbacks - * - * @return void - */ - public function dontShowTranslationFallbacks(): void - { - $this->fallback_translations = false; - } - - /** - * A search scope - * - * @param $query - * @param $field - * @param $search - * @param null $locale - * @return mixed - */ - public function scopeTranslationsSearch($query, $field, $search, $locale = null): mixed - { - if (! $locale) { - $locale = app()->getLocale(); - } - - // case insensitive search https://sarav.co/case-insensitive-search-in-mysql-json-columns - $query->where(function ($query) use ($search, $locale, $field) { - $query->whereRaw('lower(json_unquote(json_extract(`translations`, \'$."' . $field . '"\'))) like ?', ['%' . strtolower($search) . '%']) - ->orWhere($field, 'like', '%' . $search . '%'); - }); - - return $query; - } - - /** - * Locale scope to return where lang == current locale - * - * @param $query - * @param string|null $locale - * @return mixed - */ - public function scopeOfLocale($query, string $locale = null): mixed - { - if (! $locale) { - $locale = app()->getLocale(); - } - - return $query->where('lang', $locale) - ->orWhereNotNull('translations'); - } - - public function scopeNotHiddenOfLocale($query, string $locale = null) - { - if (! $locale) { - $locale = app()->getLocale(); - } - - return $query->where('lang', $locale) - ->orWhere(function ($query) { - return $query->whereNotNull('translations') - ->where('hide_translation', false); - }); - } - - /** - * Translate the given field to given locale. - * Fall back to default if no translation - * - * @param $field - * @param null $locale - * @param bool $fallback - * @return ?string - */ - public function translate($field, $locale = null, bool $fallback = true): ?string - { - if (! $locale) { - $locale = app()->getLocale(); - } - - if ($this->isPrimaryLocale($locale)) { - // for default locale, use the direct field value - $value = parent::getAttribute($field); - } else { - // for other locales, use translations - $value = $this->translations[$field] ?? null; - - if ($value && $this->hasCast($field) && is_string($value)) { - $value = $this->castAttribute($field, $value); - } - - // fallback to default if translation missing - if (empty($value) && $fallback) { - $value = parent::getAttribute($field); - } - } - - return $value; - } - - /** - * Translate the given field to given locale. - * Fall back to default if no translation - * - * @param $field - * @return string - */ - public function translateField($field): string - { - return $this->translate($field); - } - -// /** -// * Get the translatable fields -// * -// * @return array -// */ -// public function getTranslatables(): array -// { -// return property_exists($this, 'translatable') ? $this->translatable : []; -// } - - /** - * Get the non translatable fields - * - * @return array - */ - public function getNonTranslatables(): array - { - $all_fields = \Schema::getColumnListing($this->getTable()); - - $hide = array_merge($this->getTranslatables(), $this->getFieldsIgnoredForTranslation()); - - return array_values(array_diff($all_fields, $hide)); - } - - /** - * Check whether the given field is translatable - * - * @param string $field - * @return boolean - */ - public function isTranslatable(string $field): bool - { - return in_array($field, $this->getTranslatables()); - } - - /** - * Set translation for given attribute name - * - * @param string $field - * @param string $locale - * @param string $translation - * @return void - */ - public function setTranslation(string $field, string $locale, string $translation): void - { - if (! $this->isTranslatable($field)) { - return; - } - - if (! $this->isAllowedTranslationLocale($locale)) { - return; - } - - if ($this->isPrimaryLocale($locale)) { - // directly save to the db field value if default locale - parent::setAttribute($field, $translation); - } else { - // for other locales, save to the translations - $this->setTranslationAttributeValue($field, $locale, $translation); - - // if it's a new model and the default value is not set, set the default - if ((! $this->exists) && (! ($this->attributes[$field] ?? null))) { - parent::setAttribute($field, $translation); - } - } - } - - /** - * Check if is primary locale - * - * @param null $locale - * @return bool - */ - public function isPrimaryLocale($locale = null): bool - { - if (! $locale) { - $locale = app()->getLocale(); - } - - return empty($this->lang) || $this->lang == $locale; - } - - /** - * Set translation attribute value - * - * @param $attribute - * @param $locale - * @param string $translation - */ - public function setTranslationAttributeValue($attribute, $locale, string $translation): void - { - $translations = Arr::wrap($this->translations); - - $translations[$attribute] = $translation; - - $this->translations = $translations; - } - - /** - * Clear the translations for the given locale or all - * - */ - public function clearTranslations(): void - { - $this->translation = null; - } - - /** - * Check if has Translations - * - * @param null $locale - * @return bool - */ - public function hasTranslations($locale = null): bool - { - if (! $locale) { -// $locale = $this->getDefaultTranslationLocale(); - } - - if ($this->lang == $locale) { - return true; - } - - return ! empty($this->translations); - } - - /** - * Fill translations in bulk - * - * @param array $translations - * @param null $locale - * @return mixed - */ - public function fillTranslations(array $translations, $locale = null): mixed - { - if (! $locale) { - $locale = app()->getLocale(); - } - - foreach ($translations as $field => $value) { - // check whether the attribute is translatable - if ($this->isTranslatable($field)) { - $this->setTranslation($field, $locale, $value); - } else { - // check whether the attribute is suffixed - [$key, $key_locale] = $this->getFieldAndLocale($field); - - if ($key_locale && $this->isTranslatable($key)) { - $this->setTranslation($key, $key_locale, $value); - } - } - } - - return $this; - } -// -// /** -// * Returns the url -// * -// * @param string $action -// * @param string|null $locale -// * @param string $namespace -// * @return string -// */ -// public function url(string $action = 'show', string $locale = null, string $namespace = 'admin'): string -// { -// if (! $locale) { -// $locale = app()->getLocale(); -// } -// -// $controller = Str::lower(Str::plural(Str::kebab(class_basename(get_class($this))))); -// $controller_action = $namespace . '.' . $controller . '.' . $action; -// -// $params = []; -// -// $params[] = $locale ?: app()->getLocale(); -// -// if (! in_array($action, ['index', 'store', 'create', 'trash'])) { -// $params[] = $this->id; -// } -// -// $url = URL::route($controller_action, $params); -// -// return $url; -// } - - /** - * Model Override functions - * -------------------------. - */ - - /** - * Convert the model's attributes to an array. - * - * @return array - */ - public function attributesToArray(): array - { - $attributes = parent::attributesToArray(); - - return $this->addTranslatableAttributesToArray($attributes); - } - - /** - * Add the translatable attributes to the attributes array. - * - * @param array $attributes - * @return array - */ - protected function addTranslatableAttributesToArray(array $attributes): array - { - foreach ($this->getTranslatables() as $key) { - if (! isset($attributes[$key])) { - continue; - } - - $attributes[$key] = $this->translate($key); - } - - return $attributes; - } - - /** - * Get an attribute from the model. - * - * @param string $key - * - * @return mixed - */ - public function getAttribute($key): mixed - { - // Add support for compoships - if (is_array($key)) { //Check for multi-columns relationship - return array_map(function ($k) { - return parent::getAttribute($k); - }, $key); - } - - - // translate if translatable - if ($this->isTranslatable($key)) { - return $this->translate($key, null, $this->shouldFallbackForTranslations()); - } - - // check if is a suffixed attribute - [$field, $locale] = $this->getFieldAndLocale($key); - - if ($locale && $this->isTranslatable($field)) { - return $this->translate($field, $locale, false); - } - - // fallback to parent - if (($attr = parent::getAttribute($key)) !== null) { - return $attr; - } - - return null; - } - - /** - * Get the value of an attribute using its mutator for array conversion. - * - * @param string $key - * @param mixed $value - * @return mixed - */ - protected function mutateAttributeForArray($key, $value): mixed - { - // check if is a suffixed attribute - [$field, $locale] = $this->getFieldAndLocale($key); - - if ($locale && $this->isTranslatable($field)) { - return $this->{$key}; - } - - return parent::mutateAttributeForArray($key, $value); - } - - /** - * Sets attribute value - * - * @param $key - * @param $value - * @return mixed - */ - public function setAttribute($key, $value): mixed - { - - // First we will check for the presence of a mutator for the set operation - // which simply lets the developers tweak the attribute as it is set on - // the model, such as "json_encoding" an listing of data for storage. - if ($this->hasSetMutator($key)) { - return $this->setMutatedAttributeValue($key, $value); - } - - // Save the translation if is translatable - if ($this->isTranslatable($key)) { - $this->setTranslation($key, app()->getLocale(), $value); - return $this; - } - - // check if is a suffixed attribute - [$field, $locale] = $this->getFieldAndLocale($key); - - if ($locale && $this->isTranslatable($field)) { - $this->setTranslation($field, $locale, $value); - return $this; - } - - // parent call. - return parent::setAttribute($key, $value); - } - - /** - * Mutate a translated attribute value - * - * @param string $field - * @param mixed $value - */ - public function mutateTranslationAttributeValue(string $field, mixed $value): void - { - if ($this->isJsonCastable($field) && $value) { - $value = $this->castAttributeAsJson($field, $value); - } - - if (! $this->isTranslatable($field)) { - $this->attributes[$field] = $value; - return; - } - - $locale = app()->getLocale(); - - if ($this->isDefaultTranslationLocale($locale)) { - $this->attributes[$field] = $value; - } else { - $this->setTranslationAttributeValue($field, $locale, $value); - - // if it's a new model and the default value is not set, set the default - if ((! $this->exists) && (! parent::getAttribute($value))) { - $this->attributes[$field] = $value; - } - } - } - - /** - * Get the current lang for the record - * - * @return string - */ - public function getCurrentLangAttribute(): string - { - $locale = app()->getLocale(); - - if ($this->hasTranslations($locale)) { - return $locale; - } - - return $this->lang; - } - - /** - * - * @param $locale - * @return $this|bool - */ - public function getTranslation($locale): bool|static - { - if ($this->lang == $locale) { - return $this; - } - - if ($this->lang != $locale && $this->translations) { - return true; - } - } - - public function isTranslationHidden($locale): bool - { - return $this->lang != $locale && $this->hide_translation; - } - - /** - * Get the field and locale for magic attribute - * - * @param string $attribute - * @return array [field, locale] - */ - public function getFieldAndLocale(string $attribute): array - { - $locale = Str::afterLast($attribute, '_'); - - if (empty($locale) || (! $this->localeExists($locale))) { - return [$attribute, null]; - } - - $field = Str::beforeLast($attribute, '_'); - - return [$field, $locale]; - } - - /** - * Check if the locale exists in the configuration - * - * @param string $locale - * @return bool - */ - public function localeExists(string $locale): bool - { - return in_array($locale, $this->getAllowedTranslationLocales()); - } - - /** - * Get the fillable attributes for the model. - * - * @return array - */ - public function getFillableTranslatables(): array - { - $translatables = $this->getTranslatables(); - $fillables = $this->getFillable(); - - $translatable_fillables = array_intersect($translatables, $fillables); - $language_codes = $this->getAllowedTranslationLocales(); - - $suffixed_fillables = []; - - foreach ($translatable_fillables as $fillable) { - foreach ($language_codes as $code) { - $suffixed_fillables[] = $fillable . '_' . $code; - } - } - - return $suffixed_fillables; - } - - /** - * Get the fillable translatable attributes of a given array. - * - * @param array $attributes - * @return array - */ - protected function fillableTranslatablesFromArray(array $attributes): array - { - return array_intersect_key($attributes, array_flip($this->getFillableTranslatables())); - } - - /** - * First fill the suffixed translatables - * Then fill the main attributes - * - * @param array $attributes - * @return $this - * - * @throws MassAssignmentException - */ - public function fill(array $attributes): static - { - $fillable_translatables = $this->fillableTranslatablesFromArray($attributes); - - foreach ($fillable_translatables as $key => $value) { - $this->setAttribute($key, $value); - } - - return parent::fill($attributes); - } - - /** - * Add appends to translatable fields - * - * @param array $appends - * @return array - */ - public function addTranslationAppends(array $appends): array - { - $language_codes = $this->getAllowedTranslationLocales(); - - $translatables = $this->getTranslatables(); - $converted_appends = $appends; - - foreach ($translatables as $field) { - foreach ($language_codes as $code) { - $converted_appends[$field . '_' . $code] = [$field]; - } - } - - return $converted_appends; - } - - public function getIsTranslationAttribute(): bool - { - return $this->lang != app()->getLocale(); - } -} diff --git a/tests/Unit/DbTranslatable/IsDbTranslatableTest.php b/tests/Unit/DbTranslatable/IsDbTranslatableTest.php index e6b5ac2..a368bab 100644 --- a/tests/Unit/DbTranslatable/IsDbTranslatableTest.php +++ b/tests/Unit/DbTranslatable/IsDbTranslatableTest.php @@ -135,9 +135,25 @@ public function it_can_translate_field_via_translate_function() $this->assertEquals('Mee dhivehi title eh', $post->translate('title', 'dv')); $this->assertEquals('Mee dhivehi liyumeh', $post->translate('body', 'dv')); + $this->assertNull($post->translate('slug', 'dv', false)); + + $tmp = app()->getLocale(); + app()->setLocale('en'); + $this->assertEquals('This is an English title', $post->translate('title')); + $this->assertEquals('This is an English body', $post->translate('body')); + $this->assertEquals('this-is-an-english-slug', $post->translate('slug')); + + app()->setLocale('dv'); + $this->assertEquals('Mee dhivehi title eh', $post->translate('title')); + $this->assertEquals('Mee dhivehi liyumeh', $post->translate('body')); + $this->assertNull($post->translate('slug', fallback: false)); + app()->setLocale($tmp); $this->assertEquals('Kore wa taitorudesu', $post->translate('title', 'jp')); $this->assertEquals('Kore wa kijidesu', $post->translate('body', 'jp')); + $this->assertNull($post->translate('slug', 'jp', false)); + + $this->assertNull($post->translate('slug', 'fr', false)); } /** @test */ @@ -213,6 +229,33 @@ public function it_can_translate_field_via_locale_change() app()->setLocale($tmp); } + /** @test */ + public function it_can_translate_fields_via_compoships() + { + $post = Post::factory()->withAuthor()->create([ + 'lang' => 'en', + ]); + $post_dv = Post::factory()->withAuthor()->create([ + 'lang' => 'dv', + 'title' => 'Mee dhivehi title eh', + 'slug' => 'mee-dhivehi-slug-eh', + 'body' => 'Mee dhivehi liyumeh', + ]); + $post_jp = Post::factory()->withAuthor()->create([ + 'lang' => 'jp', + 'title' => 'Kore wa taitorudesu', + 'slug' => 'kore-wa-namekujidesu', + 'body' => 'Kore wa kijidesu', + ]); + + $tmp = app()->getLocale(); + app()->setLocale('dv'); + [$title, $body] = $post->getAttribute(['title', 'body']); + $this->assertEquals('Mee dhivehi title eh', $title); + $this->assertEquals('Mee dhivehi liyumeh', $body); + app()->setLocale($tmp); + } + /** @test */ public function it_can_check_if_given_field_is_translatable() { @@ -283,7 +326,9 @@ public function it_can_clear_translations_for_all_locales() /** @test */ public function it_can_check_if_any_translation_for_a_specific_locale() { - $post = Post::factory()->withAuthor()->create(); + $post = Post::factory()->withAuthor()->create([ + 'lang' => 'en' + ]); $post_dv = Post::factory()->withAuthor()->create([ 'lang' => 'dv', 'title' => 'Mee dhivehi title eh', @@ -299,6 +344,11 @@ public function it_can_check_if_any_translation_for_a_specific_locale() $this->assertFalse($post->hasTranslation('fr')); $this->assertTrue($post->hasTranslation('dv')); + $this->assertTrue($post->hasTranslation('en')); + $tmp = app()->getLocale(); + app()->setLocale('dv'); + $this->assertTrue($post->hasTranslation()); + app()->setLocale($tmp); } /** @test */ diff --git a/tests/Unit/JsonTranslatable/IsJsonTranslatableTest.php b/tests/Unit/JsonTranslatable/IsJsonTranslatableTest.php index 763bf01..eab4c45 100644 --- a/tests/Unit/JsonTranslatable/IsJsonTranslatableTest.php +++ b/tests/Unit/JsonTranslatable/IsJsonTranslatableTest.php @@ -82,6 +82,35 @@ public function it_can_check_if_is_a_non_translatable_pivot() $this->assertTrue($article->isNonTranslatablePivot('author_id')); } + /** @test */ + public function it_can_get_the_default_translation_locale() + { + $article = Article::factory()->withAuthor()->create([ + 'lang' => 'en', + ]); + + $this->assertFalse($article->isDefaultTranslationLocale('fr')); + $this->assertTrue($article->isDefaultTranslationLocale('en')); + } + + /** @test */ + public function it_can_get_allowed_translation_locales() + { + $article = Article::factory()->withAuthor()->create(); + + $this->assertEquals(['en', 'dv', 'jp'], $article->getAllowedTranslationLocales()); + } + + /** @test */ + public function it_can_check_if_given_locale_is_allowed() + { + $article = Article::factory()->withAuthor()->create(); + + $this->assertTrue($article->isAllowedTranslationLocale('en')); + $this->assertTrue($article->isAllowedTranslationLocale('dv')); + $this->assertFalse($article->isAllowedTranslationLocale('fr')); + } + /** @test */ public function it_can_translate_field_via_translate_function() { @@ -324,33 +353,4 @@ public function it_can_check_if_is_default_translation_locale() $this->assertFalse($article->isDefaultTranslationLocale('fr')); $this->assertTrue($article->isDefaultTranslationLocale('en')); } - - /** @test */ - public function it_can_get_the_default_translation_locale() - { - $article = Article::factory()->withAuthor()->create([ - 'lang' => 'en', - ]); - - $this->assertFalse($article->isDefaultTranslationLocale('fr')); - $this->assertTrue($article->isDefaultTranslationLocale('en')); - } - - /** @test */ - public function it_can_get_allowed_translation_locales() - { - $article = Article::factory()->withAuthor()->create(); - - $this->assertEquals(['en', 'dv', 'jp'], $article->getAllowedTranslationLocales()); - } - - /** @test */ - public function it_can_check_if_given_locale_is_allowed() - { - $article = Article::factory()->withAuthor()->create(); - - $this->assertTrue($article->isAllowedTranslationLocale('en')); - $this->assertTrue($article->isAllowedTranslationLocale('dv')); - $this->assertFalse($article->isAllowedTranslationLocale('fr')); - } } From 63ca262b67871b3e9a8904626751bdc8904b119f Mon Sep 17 00:00:00 2001 From: Github Actions Bot <41898282+github-actions[bot]@users.noreply.github.com> Date: Sun, 23 Feb 2025 05:32:17 +0000 Subject: [PATCH 10/24] Update code coverage badge --- .github/coverage.svg | 6 +- clover.xml | 525 ++++++++----------------------------------- 2 files changed, 99 insertions(+), 432 deletions(-) diff --git a/.github/coverage.svg b/.github/coverage.svg index e47662c..681950c 100644 --- a/.github/coverage.svg +++ b/.github/coverage.svg @@ -8,13 +8,13 @@ - + coverage - - 26 % + + 89 % \ No newline at end of file diff --git a/clover.xml b/clover.xml index 45196f1..8a56d6f 100644 --- a/clover.xml +++ b/clover.xml @@ -1,9 +1,9 @@ - - + + - + @@ -13,45 +13,45 @@ - - - - - - - - - - - - + + + + + + + + + + + + - - - - - - - - - + + + + + + + + + - - + + - - - + + + - + @@ -59,65 +59,67 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - + + - - + - - - - - - - - - - + + + + + + + + + + + - - - - - + + - + + + + + + + + @@ -164,9 +166,9 @@ - - - + + + @@ -176,350 +178,15 @@ - - - - - - - + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + From c3f60dfd89c0b2652f4bcb445852b60593e01667 Mon Sep 17 00:00:00 2001 From: WovenCoast Date: Sun, 23 Feb 2025 10:45:00 +0500 Subject: [PATCH 11/24] docs: update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 1ea42a8..0c882ee 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ [![Latest Version on Packagist](https://img.shields.io/packagist/v/javaabu/translatable.svg?style=flat-square)](https://packagist.org/packages/javaabu/translatable) [![Test Status](../../actions/workflows/run-tests.yml/badge.svg)](../../actions/workflows/run-tests.yml) [![Total Downloads](https://img.shields.io/packagist/dt/javaabu/translatable.svg?style=flat-square)](https://packagist.org/packages/javaabu/translatable) - +![Code Coverage](./.github/coverage.svg) ## Introduction From d94a4d1652adf8e5101782c149d6f0163d772456 Mon Sep 17 00:00:00 2001 From: WovenCoast Date: Sun, 23 Feb 2025 12:05:30 +0500 Subject: [PATCH 12/24] coverage: improve code coverage --- src/DbTranslatable/IsDbTranslatable.php | 48 +++------ tests/TestSupport/Factories/PostFactory.php | 2 +- .../DbTranslatable/IsDbTranslatableTest.php | 98 ++++++++++++++++++- 3 files changed, 112 insertions(+), 36 deletions(-) diff --git a/src/DbTranslatable/IsDbTranslatable.php b/src/DbTranslatable/IsDbTranslatable.php index 9e6d0e8..bfb10fe 100644 --- a/src/DbTranslatable/IsDbTranslatable.php +++ b/src/DbTranslatable/IsDbTranslatable.php @@ -20,17 +20,17 @@ public function getFieldsIgnoredForTranslation(): array public function translations() { - return $this->hasMany($this, 'translatable_parent_id'); + return $this->hasMany(self::class, 'translatable_parent_id', 'id'); } public function defaultTranslation() { - return $this->belongsTo($this, 'translatable_parent_id'); + return $this->belongsTo(self::class, 'translatable_parent_id', 'id'); } public function isDefaultTranslation(): bool { - return $this->getAttributeValue('translatable_parent_id') != null; + return $this->getAttributeValue('translatable_parent_id') == null; } public function translate(string $field, ?string $locale = null, bool $fallback = true): mixed @@ -55,23 +55,15 @@ public function translate(string $field, ?string $locale = null, bool $fallback return $fallback ? $this->getAttributeValue($field) : null; } - if ($this->isDefaultTranslation()) { - // if there's no parent, this is the main one. get translations using the defined relation - $translation = $this->translations()->where('lang', $locale)->first(); - dd($translation); - } else { - // otherwise it's already a translation, get all translations including the parent - $translation = self::query()->where([ - 'lang' => $locale, - 'id' => $this->translatable_parent_id, - ])->orWhere([ - 'lang' => $locale, - 'parent_id' => $this->translatable_parent_id, - ])->first(); + // get default translation to check its language first + $defaultTranslation = $this->isDefaultTranslation() ? $this : $this->defaultTranslation; + if ($defaultTranslation->lang == $locale) { + return $defaultTranslation->getAttributeValue($field); } + // attempt to fetch the first translation within translatable rows + $translation = $defaultTranslation->translations()->where('lang', $locale)->first(); - - // fallback if the translation doesn't exist + // fallback if the translation doesn't exist in any of the translated rows if (!$translation) { return $fallback ? $this->getAttributeValue($field) : null; } @@ -89,28 +81,20 @@ public function getDefaultTranslationLocale(): string public function clearTranslations(?string $locale = null): void { - if (is_null($locale)) { + if (! $locale) { // nuke all except the main one $parent_id = $this->isDefaultTranslation() ? $this->id : $this->translatable_parent_id; self::query()->where('translatable_parent_id', $parent_id)->withTrashed()->forceDelete(); } else { // check the current one lang, if it's correct delete it if ($this->lang == $locale) { - if (!$this->isDefaultTranslation()) { - $this->forceDelete(); - } + $this->delete(); } else { - if ($this->isDefaultTranslation()) { - $translation = $this->translations()->where('lang', $locale); - // deleting the default translation will soft lock - $translation->delete(); - } else { - $translation = self::query()->where([ - 'translatable_parent_id' => $this->translatable_parent_id, - 'lang' => $locale, - ]); - $translation->forceDelete(); + $defaultTranslation = $this->isDefaultTranslation() ? $this : $this->defaultTranslation; + if ($defaultTranslation->lang == $locale) { + $defaultTranslation->delete(); } + $defaultTranslation->translations()->where('lang', $locale)->delete(); } } } diff --git a/tests/TestSupport/Factories/PostFactory.php b/tests/TestSupport/Factories/PostFactory.php index df4c021..69c0f59 100644 --- a/tests/TestSupport/Factories/PostFactory.php +++ b/tests/TestSupport/Factories/PostFactory.php @@ -14,7 +14,7 @@ class PostFactory extends Factory public function definition(): array { return [ - 'title' => $this->faker->title(), + 'title' => $this->faker->sentence(), 'slug' => $this->faker->slug(), 'body' => $this->faker->paragraph(5), 'lang' => $this->faker->locale(), diff --git a/tests/Unit/DbTranslatable/IsDbTranslatableTest.php b/tests/Unit/DbTranslatable/IsDbTranslatableTest.php index a368bab..078a573 100644 --- a/tests/Unit/DbTranslatable/IsDbTranslatableTest.php +++ b/tests/Unit/DbTranslatable/IsDbTranslatableTest.php @@ -125,18 +125,29 @@ public function it_can_translate_field_via_translate_function() 'title' => 'Mee dhivehi title eh', 'slug' => 'mee-dhivehi-slug-eh', 'body' => 'Mee dhivehi liyumeh', + 'translatable_parent_id' => $post->id, ]); $post_jp = Post::factory()->withAuthor()->create([ 'lang' => 'jp', 'title' => 'Kore wa taitorudesu', 'slug' => 'kore-wa-namekujidesu', 'body' => 'Kore wa kijidesu', + 'translatable_parent_id' => $post->id, ]); + // it can fetch translations from the default post $this->assertEquals('Mee dhivehi title eh', $post->translate('title', 'dv')); $this->assertEquals('Mee dhivehi liyumeh', $post->translate('body', 'dv')); $this->assertNull($post->translate('slug', 'dv', false)); + // it can fetch translations from a translatable post row as well + $this->assertEquals('Mee dhivehi title eh', $post_jp->translate('title', 'dv')); + $this->assertEquals('Mee dhivehi liyumeh', $post_jp->translate('body', 'dv')); + + // it can fetch translation from default translation using a translatable post row as well + $this->assertEquals('This is an English title', $post_jp->translate('title', 'en')); + $this->assertEquals('This is an English body', $post_jp->translate('body', 'en')); + $tmp = app()->getLocale(); app()->setLocale('en'); $this->assertEquals('This is an English title', $post->translate('title')); @@ -170,6 +181,7 @@ public function it_can_translate_field_via_translate_function_without_fallback() 'title' => 'Mee dhivehi title eh', 'slug' => 'mee-dhivehi-slug-eh', 'body' => 'Mee dhivehi liyumeh', + 'translatable_parent_id' => $post->id, ]); $this->assertEquals('This is an English title', $post->translate('title', 'jp')); @@ -190,12 +202,14 @@ public function it_can_translate_field_via_magic_method() 'title' => 'Mee dhivehi title eh', 'slug' => 'mee-dhivehi-slug-eh', 'body' => 'Mee dhivehi liyumeh', + 'translatable_parent_id' => $post->id, ]); $post_jp = Post::factory()->withAuthor()->create([ 'lang' => 'jp', 'title' => 'Kore wa taitorudesu', 'slug' => 'kore-wa-namekujidesu', 'body' => 'Kore wa kijidesu', + 'translatable_parent_id' => $post->id, ]); $this->assertEquals('Mee dhivehi title eh', $post->title_dv); @@ -214,12 +228,14 @@ public function it_can_translate_field_via_locale_change() 'title' => 'Mee dhivehi title eh', 'slug' => 'mee-dhivehi-slug-eh', 'body' => 'Mee dhivehi liyumeh', + 'translatable_parent_id' => $post->id, ]); $post_jp = Post::factory()->withAuthor()->create([ 'lang' => 'jp', 'title' => 'Kore wa taitorudesu', 'slug' => 'kore-wa-namekujidesu', 'body' => 'Kore wa kijidesu', + 'translatable_parent_id' => $post->id, ]); $tmp = app()->getLocale(); @@ -240,12 +256,14 @@ public function it_can_translate_fields_via_compoships() 'title' => 'Mee dhivehi title eh', 'slug' => 'mee-dhivehi-slug-eh', 'body' => 'Mee dhivehi liyumeh', + 'translatable_parent_id' => $post->id, ]); $post_jp = Post::factory()->withAuthor()->create([ 'lang' => 'jp', 'title' => 'Kore wa taitorudesu', 'slug' => 'kore-wa-namekujidesu', 'body' => 'Kore wa kijidesu', + 'translatable_parent_id' => $post->id, ]); $tmp = app()->getLocale(); @@ -265,12 +283,14 @@ public function it_can_check_if_given_field_is_translatable() 'title' => 'Mee dhivehi title eh', 'slug' => 'mee-dhivehi-slug-eh', 'body' => 'Mee dhivehi liyumeh', + 'translatable_parent_id' => $post->id, ]); $post_jp = Post::factory()->withAuthor()->create([ 'lang' => 'jp', 'title' => 'Kore wa taitorudesu', 'slug' => 'kore-wa-namekujidesu', 'body' => 'Kore wa kijidesu', + 'translatable_parent_id' => $post->id, ]); $this->assertTrue($post->isTranslatable('title')); @@ -279,27 +299,93 @@ public function it_can_check_if_given_field_is_translatable() /** @test */ public function it_can_clear_translations_for_one_locale() { - $post = Post::factory()->withAuthor()->create(); + $post = Post::factory()->withAuthor()->create([ + 'lang' => 'en' + ]); $post_dv = Post::factory()->withAuthor()->create([ 'lang' => 'dv', 'title' => 'Mee dhivehi title eh', 'slug' => 'mee-dhivehi-slug-eh', 'body' => 'Mee dhivehi liyumeh', + 'translatable_parent_id' => $post->id, ]); $post_jp = Post::factory()->withAuthor()->create([ 'lang' => 'jp', 'title' => 'Kore wa taitorudesu', 'slug' => 'kore-wa-namekujidesu', 'body' => 'Kore wa kijidesu', + 'translatable_parent_id' => $post->id, ]); $post->clearTranslations('dv'); // Ensure that dv is gone while jp is still there - $this->assertEmpty($post->title_dv); + $this->assertEmpty($post?->title_dv); $this->assertEquals('Kore wa taitorudesu', $post->title_jp); } + /** @test */ + public function it_can_clear_translations_for_default_locale() + { + $post = Post::factory()->withAuthor()->create([ + 'lang' => 'en' + ]); + $post_dv = Post::factory()->withAuthor()->create([ + 'lang' => 'dv', + 'title' => 'Mee dhivehi title eh', + 'slug' => 'mee-dhivehi-slug-eh', + 'body' => 'Mee dhivehi liyumeh', + 'translatable_parent_id' => $post->id, + ]); + $post_jp = Post::factory()->withAuthor()->create([ + 'lang' => 'jp', + 'title' => 'Kore wa taitorudesu', + 'slug' => 'kore-wa-namekujidesu', + 'body' => 'Kore wa kijidesu', + 'translatable_parent_id' => $post->id, + ]); + + $post->clearTranslations('en'); + + // refresh post on memory + $post = Post::find($post->id); + + // Ensure that everything is gone because it's the main language + $this->assertEmpty($post?->title_en); + $this->assertEmpty($post?->title_jp); + } + + /** @test */ + public function it_can_clear_translations_for_locale_via_translatable_parent() + { + $post = Post::factory()->withAuthor()->create([ + 'lang' => 'en' + ]); + $post_dv = Post::factory()->withAuthor()->create([ + 'lang' => 'dv', + 'title' => 'Mee dhivehi title eh', + 'slug' => 'mee-dhivehi-slug-eh', + 'body' => 'Mee dhivehi liyumeh', + 'translatable_parent_id' => $post->id, + ]); + $post_jp = Post::factory()->withAuthor()->create([ + 'lang' => 'jp', + 'title' => 'Kore wa taitorudesu', + 'slug' => 'kore-wa-namekujidesu', + 'body' => 'Kore wa kijidesu', + 'translatable_parent_id' => $post->id, + ]); + + $post_jp->clearTranslations('en'); + + // refresh post on memory + $post = Post::find($post->id); + + // Ensure that everything is gone because it's the main language + $this->assertEmpty($post?->title_en); + $this->assertEmpty($post?->title_jp); + } + /** @test */ public function it_can_clear_translations_for_all_locales() { @@ -309,12 +395,14 @@ public function it_can_clear_translations_for_all_locales() 'title' => 'Mee dhivehi title eh', 'slug' => 'mee-dhivehi-slug-eh', 'body' => 'Mee dhivehi liyumeh', + 'translatable_parent_id' => $post->id, ]); $post_jp = Post::factory()->withAuthor()->create([ 'lang' => 'jp', 'title' => 'Kore wa taitorudesu', 'slug' => 'kore-wa-namekujidesu', 'body' => 'Kore wa kijidesu', + 'translatable_parent_id' => $post->id, ]); $post->clearTranslations(); @@ -334,16 +422,18 @@ public function it_can_check_if_any_translation_for_a_specific_locale() 'title' => 'Mee dhivehi title eh', 'slug' => 'mee-dhivehi-slug-eh', 'body' => 'Mee dhivehi liyumeh', + 'translatable_parent_id' => $post->id, ]); $post_jp = Post::factory()->withAuthor()->create([ 'lang' => 'jp', 'title' => 'Kore wa taitorudesu', 'slug' => 'kore-wa-namekujidesu', 'body' => 'Kore wa kijidesu', + 'translatable_parent_id' => $post->id, ]); $this->assertFalse($post->hasTranslation('fr')); - $this->assertTrue($post->hasTranslation('dv')); + $this->assertTrue($post_jp->hasTranslation('dv')); $this->assertTrue($post->hasTranslation('en')); $tmp = app()->getLocale(); app()->setLocale('dv'); @@ -362,12 +452,14 @@ public function it_can_check_if_is_default_translation_locale() 'title' => 'Mee dhivehi title eh', 'slug' => 'mee-dhivehi-slug-eh', 'body' => 'Mee dhivehi liyumeh', + 'translatable_parent_id' => $post->id, ]); $post_jp = Post::factory()->withAuthor()->create([ 'lang' => 'jp', 'title' => 'Kore wa taitorudesu', 'slug' => 'kore-wa-namekujidesu', 'body' => 'Kore wa kijidesu', + 'translatable_parent_id' => $post->id, ]); $this->assertFalse($post->isDefaultTranslationLocale('fr')); From a559b6d91c834ec57e9a4c4d7541871ffedd006c Mon Sep 17 00:00:00 2001 From: Github Actions Bot <41898282+github-actions[bot]@users.noreply.github.com> Date: Sun, 23 Feb 2025 07:06:15 +0000 Subject: [PATCH 13/24] Update code coverage badge --- .github/coverage.svg | 6 +- clover.xml | 149 ++++++++++++++++++++----------------------- 2 files changed, 72 insertions(+), 83 deletions(-) diff --git a/.github/coverage.svg b/.github/coverage.svg index 681950c..1db4533 100644 --- a/.github/coverage.svg +++ b/.github/coverage.svg @@ -8,13 +8,13 @@ - + coverage - - 89 % + + 100 % \ No newline at end of file diff --git a/clover.xml b/clover.xml index 8a56d6f..0875212 100644 --- a/clover.xml +++ b/clover.xml @@ -1,6 +1,6 @@ - - + + @@ -15,56 +15,56 @@ - - - - - - - - - - + + + + + + + + + + - - + + - + - - + + - + - - - + + + - + - - - - - - - + + + + + + + @@ -73,53 +73,42 @@ - - - + + + - - - - - - - - - - - - - + + + + + + + + + + + + - - - - - - - + + + + - - - + + + + + + - - - - - - - - - - - - - - + + + + @@ -166,9 +155,9 @@ - - - + + + @@ -178,15 +167,15 @@ - - - - - - - + + + + + + + - + From c973200aaf3e9dce6d0cdf047202a5cc1fa0cbcf Mon Sep 17 00:00:00 2001 From: WovenCoast Date: Sun, 23 Feb 2025 12:23:19 +0500 Subject: [PATCH 14/24] docs: update README.md --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 0c882ee..3a58807 100644 --- a/README.md +++ b/README.md @@ -43,6 +43,7 @@ If you discover any security related issues, please email [info@javaabu.com](mai - [Javaabu Pvt. Ltd.](https://github.com/javaabu) - [Arushad Ahmed (@dash8x)](http://arushad.com) - [Xylam (@Xylam)](https://github.com/Xylam) +- [FlameXode (@WovenCoast)](https://github.com/WovenCoast) - [All Contributors](../../contributors) ## License From a22b78cddf4c2b6e8742311a971ad5810dea8f01 Mon Sep 17 00:00:00 2001 From: WovenCoast Date: Sun, 23 Feb 2025 13:05:35 +0500 Subject: [PATCH 15/24] coverage: add addTranslation function --- src/DbTranslatable/IsDbTranslatable.php | 24 +++++++++++++++++++ src/JsonTranslatable/IsJsonTranslatable.php | 20 ++++++++++++++++ src/Translatable.php | 9 +++++++ .../DbTranslatable/IsDbTranslatableTest.php | 18 ++++++++++++++ .../IsJsonTranslatableTest.php | 19 +++++++++++++++ 5 files changed, 90 insertions(+) diff --git a/src/DbTranslatable/IsDbTranslatable.php b/src/DbTranslatable/IsDbTranslatable.php index bfb10fe..cf2b9fc 100644 --- a/src/DbTranslatable/IsDbTranslatable.php +++ b/src/DbTranslatable/IsDbTranslatable.php @@ -124,4 +124,28 @@ public function hasTranslation(?string $locale = null): bool return $translation->exists(); } } + + public function addTranslation(string $locale, array $fields = []): static + { + $parent_id = $this->isDefaultTranslation() ? $this->id : $this->translatable_parent_id; + + $newTranslation = new self(); + + foreach ($this->getAllNonTranslatables() as $nonTranslatable) { + if ($nonTranslatable == "id") continue; + $newTranslation->setAttribute($nonTranslatable, $this->getAttributeValue($nonTranslatable)); + } + + foreach ($fields as $field => $value) { + $newTranslation->setAttribute($field, $value); + } + + $newTranslation->setAttribute('translatable_parent_id', $parent_id); + $newTranslation->setAttribute('lang', $locale); + + // TODO: should it save automatically? + $newTranslation->save(); + + return $newTranslation; + } } diff --git a/src/JsonTranslatable/IsJsonTranslatable.php b/src/JsonTranslatable/IsJsonTranslatable.php index ef0c4e7..4c4ef1f 100644 --- a/src/JsonTranslatable/IsJsonTranslatable.php +++ b/src/JsonTranslatable/IsJsonTranslatable.php @@ -116,4 +116,24 @@ public function hasTranslation(?string $locale = null): bool return isset($this->translations[$locale]); } + + /** + * Add a new locale to this object + * + * @param string $locale + * @param array $fields + * @return $this + */ + public function addTranslation(string $locale, array $fields = []): static + { + /** @var array $translations */ + $translations = $this->translations ?? []; + $translations[$locale] = array_merge(array_key_exists($locale, $translations) ? $translations[$locale] : [], $fields, ['lang' => $locale]); + $this->translations = $translations; + + // TODO: should it save automatically? + $this->save(); + + return $this; + } } diff --git a/src/Translatable.php b/src/Translatable.php index 340e1fa..a441a69 100644 --- a/src/Translatable.php +++ b/src/Translatable.php @@ -115,4 +115,13 @@ public function getAllowedTranslationLocales(): array; * @return boolean */ public function isAllowedTranslationLocale(string $locale): bool; + + /** + * Add a new locale to this object + * + * @param string $locale + * @param array $fields + * @return $this + */ + public function addTranslation(string $locale, array $fields = []): static; } diff --git a/tests/Unit/DbTranslatable/IsDbTranslatableTest.php b/tests/Unit/DbTranslatable/IsDbTranslatableTest.php index 078a573..acc036b 100644 --- a/tests/Unit/DbTranslatable/IsDbTranslatableTest.php +++ b/tests/Unit/DbTranslatable/IsDbTranslatableTest.php @@ -465,4 +465,22 @@ public function it_can_check_if_is_default_translation_locale() $this->assertFalse($post->isDefaultTranslationLocale('fr')); $this->assertTrue($post->isDefaultTranslationLocale('en')); } + + /** @test */ + public function it_can_add_new_translation_locales() + { + $post = Post::factory()->withAuthor()->create([ + 'lang' => 'en', + ]); + + $translation = $post->addTranslation('dv', [ + 'title' => 'Mee dhivehi title eh', + 'slug' => 'mee-dhivehi-slug-eh', + 'body' => 'Mee dhivehi liyumeh', + ]); + + $translation->save(); + + $this->assertEquals('Mee dhivehi title eh', $post->title_dv); + } } diff --git a/tests/Unit/JsonTranslatable/IsJsonTranslatableTest.php b/tests/Unit/JsonTranslatable/IsJsonTranslatableTest.php index eab4c45..f4fdd10 100644 --- a/tests/Unit/JsonTranslatable/IsJsonTranslatableTest.php +++ b/tests/Unit/JsonTranslatable/IsJsonTranslatableTest.php @@ -5,6 +5,7 @@ use Illuminate\Foundation\Testing\RefreshDatabase; use Javaabu\Translatable\Tests\TestCase; use Javaabu\Translatable\Tests\TestSupport\Models\Article; +use function PHPUnit\Framework\assertEquals; class IsJsonTranslatableTest extends TestCase { @@ -353,4 +354,22 @@ public function it_can_check_if_is_default_translation_locale() $this->assertFalse($article->isDefaultTranslationLocale('fr')); $this->assertTrue($article->isDefaultTranslationLocale('en')); } + + /** @test */ + public function it_can_add_new_translation_locales() + { + $article = Article::factory()->withAuthor()->create([ + 'lang' => 'en', + ]); + + $translation = $article->addTranslation('dv', [ + 'title' => 'Mee dhivehi title eh', + 'slug' => 'mee-dhivehi-slug-eh', + 'body' => 'Mee dhivehi liyumeh', + ]); + + $translation->save(); + + assertEquals('Mee dhivehi title eh', $article->title_dv); + } } From b3fd6a758c5dd10e84393f04d3c00adc1dd0dcfd Mon Sep 17 00:00:00 2001 From: Github Actions Bot <41898282+github-actions[bot]@users.noreply.github.com> Date: Sun, 23 Feb 2025 08:06:21 +0000 Subject: [PATCH 16/24] Update code coverage badge --- clover.xml | 176 +++++++++++++++++++++++++++++------------------------ 1 file changed, 97 insertions(+), 79 deletions(-) diff --git a/clover.xml b/clover.xml index 0875212..5b5236b 100644 --- a/clover.xml +++ b/clover.xml @@ -1,87 +1,87 @@ - - + + - - - - - - + + + + + + - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + - + - - - - + + + + - - - + + + - + - - - - - - - + + + + + + + - - - - + + + + - + - + - + - - + + - - + + - - - + + + @@ -108,32 +108,44 @@ - + + + + + + + + + + + + + - + - - + + - + - + - + - - + + - - - + + + @@ -149,33 +161,39 @@ - + + + + + + + - - - + + + - + - - - - - - - + + + + + + + - + From fa0a757009642b20b715a4dafe6541e869d9db90 Mon Sep 17 00:00:00 2001 From: WovenCoast Date: Sun, 23 Feb 2025 14:25:32 +0500 Subject: [PATCH 17/24] fix: add translation for locales that are not allowed --- src/DbTranslatable/IsDbTranslatable.php | 8 ++++++++ .../LanguageNotAllowedException.php | 18 ++++++++++++++++++ src/JsonTranslatable/IsJsonTranslatable.php | 6 ++++++ .../DbTranslatable/IsDbTranslatableTest.php | 16 ++++++++++++++++ .../IsJsonTranslatableTest.php | 19 +++++++++++++++++-- 5 files changed, 65 insertions(+), 2 deletions(-) create mode 100644 src/Exceptions/LanguageNotAllowedException.php diff --git a/src/DbTranslatable/IsDbTranslatable.php b/src/DbTranslatable/IsDbTranslatable.php index cf2b9fc..b75a5d0 100644 --- a/src/DbTranslatable/IsDbTranslatable.php +++ b/src/DbTranslatable/IsDbTranslatable.php @@ -3,6 +3,7 @@ namespace Javaabu\Translatable\DbTranslatable; use Javaabu\Translatable\Abstract\IsTranslatable; +use Javaabu\Translatable\Exceptions\LanguageNotAllowedException; trait IsDbTranslatable { @@ -125,8 +126,15 @@ public function hasTranslation(?string $locale = null): bool } } + /** + * @throws LanguageNotAllowedException + */ public function addTranslation(string $locale, array $fields = []): static { + if (! $this->isAllowedTranslationLocale($locale)) { + throw LanguageNotAllowedException::create($locale); + } + $parent_id = $this->isDefaultTranslation() ? $this->id : $this->translatable_parent_id; $newTranslation = new self(); diff --git a/src/Exceptions/LanguageNotAllowedException.php b/src/Exceptions/LanguageNotAllowedException.php new file mode 100644 index 0000000..fc7a47a --- /dev/null +++ b/src/Exceptions/LanguageNotAllowedException.php @@ -0,0 +1,18 @@ +isAllowedTranslationLocale($locale)) { + throw LanguageNotAllowedException::create($locale); + } + /** @var array $translations */ $translations = $this->translations ?? []; $translations[$locale] = array_merge(array_key_exists($locale, $translations) ? $translations[$locale] : [], $fields, ['lang' => $locale]); diff --git a/tests/Unit/DbTranslatable/IsDbTranslatableTest.php b/tests/Unit/DbTranslatable/IsDbTranslatableTest.php index acc036b..e639acd 100644 --- a/tests/Unit/DbTranslatable/IsDbTranslatableTest.php +++ b/tests/Unit/DbTranslatable/IsDbTranslatableTest.php @@ -3,6 +3,7 @@ namespace Javaabu\Translatable\Tests\Unit\DbTranslatable; use Illuminate\Foundation\Testing\RefreshDatabase; +use Javaabu\Translatable\Exceptions\LanguageNotAllowedException; use Javaabu\Translatable\Tests\TestCase; use Javaabu\Translatable\Tests\TestSupport\Models\Post; @@ -483,4 +484,19 @@ public function it_can_add_new_translation_locales() $this->assertEquals('Mee dhivehi title eh', $post->title_dv); } + + /** @test */ + public function it_cannot_add_translation_locales_that_are_not_allowed() + { + $this->expectException(LanguageNotAllowedException::class); + $post = Post::factory()->withAuthor()->create([ + 'lang' => 'en', + ]); + + $this->assertThrows($post->addTranslation('zh-CN', [ + 'title' => '这是一个中文标题', + 'slug' => '这是一只中国蛞蝓', + 'body' => '这是一个中国人的身体', + ]), LanguageNotAllowedException::class, 'zh-CN language not allowed'); + } } diff --git a/tests/Unit/JsonTranslatable/IsJsonTranslatableTest.php b/tests/Unit/JsonTranslatable/IsJsonTranslatableTest.php index f4fdd10..7f4f1df 100644 --- a/tests/Unit/JsonTranslatable/IsJsonTranslatableTest.php +++ b/tests/Unit/JsonTranslatable/IsJsonTranslatableTest.php @@ -3,9 +3,9 @@ namespace Javaabu\Translatable\Tests\Unit\JsonTranslatable; use Illuminate\Foundation\Testing\RefreshDatabase; +use Javaabu\Translatable\Exceptions\LanguageNotAllowedException; use Javaabu\Translatable\Tests\TestCase; use Javaabu\Translatable\Tests\TestSupport\Models\Article; -use function PHPUnit\Framework\assertEquals; class IsJsonTranslatableTest extends TestCase { @@ -370,6 +370,21 @@ public function it_can_add_new_translation_locales() $translation->save(); - assertEquals('Mee dhivehi title eh', $article->title_dv); + $this->assertEquals('Mee dhivehi title eh', $article->title_dv); + } + + /** @test */ + public function it_cannot_add_translation_locales_that_are_not_allowed() + { + $this->expectException(LanguageNotAllowedException::class); + $article = Article::factory()->withAuthor()->create([ + 'lang' => 'en', + ]); + + $this->assertThrows($article->addTranslation('zh-CN', [ + 'title' => '这是一个中文标题', + 'slug' => '这是一只中国蛞蝓', + 'body' => '这是一个中国人的身体', + ]), LanguageNotAllowedException::class, 'zh-CN language not allowed'); } } From c55e8392d16575ffa4f9993e7d4e7b99fd7405fb Mon Sep 17 00:00:00 2001 From: WovenCoast Date: Sun, 23 Feb 2025 16:05:22 +0500 Subject: [PATCH 18/24] wip: eod --- src/TranslatableServiceProvider.php | 7 +++++-- tests/TestSupport/Models/Post.php | 2 +- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/TranslatableServiceProvider.php b/src/TranslatableServiceProvider.php index 50b588a..b23062f 100644 --- a/src/TranslatableServiceProvider.php +++ b/src/TranslatableServiceProvider.php @@ -9,9 +9,12 @@ class TranslatableServiceProvider extends ServiceProvider /** * Bootstrap the application services. */ - public function boot() + public function boot(): void { // declare publishes +// $this->commands([ +// \Javaabu\Translatable\Commands\ImplementTranslatablesForModel::class, +// ]); if ($this->app->runningInConsole()) { $this->publishes([ __DIR__ . '/../config/translatable.php' => config_path('translatable.php'), @@ -22,7 +25,7 @@ public function boot() /** * Register the application services. */ - public function register() + public function register(): void { // merge package config with user defined config $this->mergeConfigFrom(__DIR__ . '/../config/translatable.php', 'translatable'); diff --git a/tests/TestSupport/Models/Post.php b/tests/TestSupport/Models/Post.php index 672d3ae..06c90a8 100644 --- a/tests/TestSupport/Models/Post.php +++ b/tests/TestSupport/Models/Post.php @@ -12,9 +12,9 @@ class Post extends Model implements Translatable { + use IsDbTranslatable; use HasFactory; use SoftDeletes; - use IsDbTranslatable; protected static function newFactory(): PostFactory { From 5f244054c55a763b915c9b5baa6fc798441c4c9f Mon Sep 17 00:00:00 2001 From: WovenCoast Date: Mon, 24 Feb 2025 12:19:43 +0500 Subject: [PATCH 19/24] feat!: add translation via setter BREAKING CHANGE: addTranslation bulk attribute add functionality has now been changed to addTranslations --- src/Abstract/IsTranslatable.php | 42 ++++++++++++- src/DbTranslatable/IsDbTranslatable.php | 35 +++++++---- src/JsonTranslatable/IsJsonTranslatable.php | 15 +++-- src/Translatable.php | 15 ++++- .../DbTranslatable/IsDbTranslatableTest.php | 59 +++++++++++++++---- .../IsJsonTranslatableTest.php | 46 +++++++++++---- 6 files changed, 168 insertions(+), 44 deletions(-) diff --git a/src/Abstract/IsTranslatable.php b/src/Abstract/IsTranslatable.php index 9a1cf08..9b66dca 100644 --- a/src/Abstract/IsTranslatable.php +++ b/src/Abstract/IsTranslatable.php @@ -3,9 +3,20 @@ namespace Javaabu\Translatable\Abstract; use Illuminate\Support\Str; +use Javaabu\Translatable\Exceptions\LanguageNotAllowedException; trait IsTranslatable { + /** + * Get all fields including pivots and fields ignored for translation + * + * @return array + */ + public function getAllAttributes(): array + { + return \Schema::getColumnListing($this->getTable()); + } + /** * Get non translatable fields without pivots and fields ignored for translation * @@ -15,7 +26,7 @@ trait IsTranslatable */ public function getNonTranslatables(): array { - $all_fields = \Schema::getColumnListing($this->getTable()); + $all_fields = $this->getAllAttributes(); $hide = array_merge($this->getTranslatables(), $this->getFieldsIgnoredForTranslation(), $this->getNonTranslatablePivots()); @@ -86,6 +97,19 @@ public function isTranslatable(string $field): bool return in_array($field, $this->getTranslatables()); } + /** + * Bulk add translatable fields + * + * @param string $locale + * @param array $fields + * @return $this + */ + public function addTranslations(string $locale, array $fields): static + { + array_map([$this, 'addTranslation'], $fields); + return $this; + } + /** * Get the field and locale for a given attribute if possible * @@ -130,4 +154,20 @@ public function getAttribute($key): mixed // fallback to parent return parent::getAttribute($key); } + + /** + * @throws LanguageNotAllowedException + */ + public function setAttribute($key, $value): mixed + { + [$field, $locale] = $this->getFieldAndLocale($key); + + if (! $locale) return parent::setAttribute($key, $value); + + if ($this->isTranslatable($field)) { + $this->addTranslation($locale, $field, $value); + } + + return parent::setAttribute($key, $value); + } } diff --git a/src/DbTranslatable/IsDbTranslatable.php b/src/DbTranslatable/IsDbTranslatable.php index b75a5d0..fcec085 100644 --- a/src/DbTranslatable/IsDbTranslatable.php +++ b/src/DbTranslatable/IsDbTranslatable.php @@ -129,29 +129,40 @@ public function hasTranslation(?string $locale = null): bool /** * @throws LanguageNotAllowedException */ - public function addTranslation(string $locale, array $fields = []): static + public function addTranslation(string $locale, string $field, string $value): static { if (! $this->isAllowedTranslationLocale($locale)) { throw LanguageNotAllowedException::create($locale); } - $parent_id = $this->isDefaultTranslation() ? $this->id : $this->translatable_parent_id; - - $newTranslation = new self(); + $defaultTranslation = $this->isDefaultTranslation() ? $this : $this->defaultTranslation; - foreach ($this->getAllNonTranslatables() as $nonTranslatable) { - if ($nonTranslatable == "id") continue; - $newTranslation->setAttribute($nonTranslatable, $this->getAttributeValue($nonTranslatable)); + // check if the default translation is already the correct locale + if ($defaultTranslation->lang == $locale) { + $defaultTranslation->setAttribute($field, $value); + $defaultTranslation->save(); + return $defaultTranslation; } - foreach ($fields as $field => $value) { - $newTranslation->setAttribute($field, $value); + // check if a translated object exists for this locale + $newTranslation = $defaultTranslation->translations()->where('lang', $locale)->first(); + + // if there is none, make a new blank translation of this object + if (! $newTranslation) { + $newTranslation = new self(); + + // copy all the attributes of the current object to the new translation + // this ensures no columns are left null + foreach ($this->getAllAttributes() as $attribute) { + // TODO: make this primary key rely on some sort of config available per model + if ($attribute == "id") continue; + $newTranslation->setAttribute($attribute, $defaultTranslation->getAttributeValue($attribute)); + } + $newTranslation->setAttribute('translatable_parent_id', $defaultTranslation->id); } - $newTranslation->setAttribute('translatable_parent_id', $parent_id); + $newTranslation->setAttribute($field, $value); $newTranslation->setAttribute('lang', $locale); - - // TODO: should it save automatically? $newTranslation->save(); return $newTranslation; diff --git a/src/JsonTranslatable/IsJsonTranslatable.php b/src/JsonTranslatable/IsJsonTranslatable.php index 414b3ec..d50dfed 100644 --- a/src/JsonTranslatable/IsJsonTranslatable.php +++ b/src/JsonTranslatable/IsJsonTranslatable.php @@ -118,15 +118,16 @@ public function hasTranslation(?string $locale = null): bool return isset($this->translations[$locale]); } + /** * Add a new locale to this object * * @param string $locale - * @param array $fields + * @param string $field + * @param string $value * @return $this - * @throws LanguageNotAllowedException */ - public function addTranslation(string $locale, array $fields = []): static + public function addTranslation(string $locale, string $field, string $value): static { if (! $this->isAllowedTranslationLocale($locale)) { throw LanguageNotAllowedException::create($locale); @@ -134,10 +135,12 @@ public function addTranslation(string $locale, array $fields = []): static /** @var array $translations */ $translations = $this->translations ?? []; - $translations[$locale] = array_merge(array_key_exists($locale, $translations) ? $translations[$locale] : [], $fields, ['lang' => $locale]); + $translations[$locale] = array_merge( + array_key_exists($locale, $translations) ? $translations[$locale] : [], + [$field => $value], + ['lang' => $locale] + ); $this->translations = $translations; - - // TODO: should it save automatically? $this->save(); return $this; diff --git a/src/Translatable.php b/src/Translatable.php index a441a69..58d9af6 100644 --- a/src/Translatable.php +++ b/src/Translatable.php @@ -6,6 +6,8 @@ namespace Javaabu\Translatable; +use Javaabu\Translatable\Exceptions\LanguageNotAllowedException; + interface Translatable { /** @@ -116,6 +118,17 @@ public function getAllowedTranslationLocales(): array; */ public function isAllowedTranslationLocale(string $locale): bool; + /** + * Add an attribute with a new locale to this object + * + * @param string $locale + * @param string $field + * @param string $value + * @return $this + * @throws LanguageNotAllowedException + */ + public function addTranslation(string $locale, string $field, string $value): static; + /** * Add a new locale to this object * @@ -123,5 +136,5 @@ public function isAllowedTranslationLocale(string $locale): bool; * @param array $fields * @return $this */ - public function addTranslation(string $locale, array $fields = []): static; + public function addTranslations(string $locale, array $fields): static; } diff --git a/tests/Unit/DbTranslatable/IsDbTranslatableTest.php b/tests/Unit/DbTranslatable/IsDbTranslatableTest.php index e639acd..b3a89eb 100644 --- a/tests/Unit/DbTranslatable/IsDbTranslatableTest.php +++ b/tests/Unit/DbTranslatable/IsDbTranslatableTest.php @@ -467,24 +467,57 @@ public function it_can_check_if_is_default_translation_locale() $this->assertTrue($post->isDefaultTranslationLocale('en')); } - /** @test */ + /** @test + * @throws LanguageNotAllowedException + */ public function it_can_add_new_translation_locales() { $post = Post::factory()->withAuthor()->create([ 'lang' => 'en', ]); - $translation = $post->addTranslation('dv', [ - 'title' => 'Mee dhivehi title eh', - 'slug' => 'mee-dhivehi-slug-eh', - 'body' => 'Mee dhivehi liyumeh', - ]); +// $translation = $article->addTranslation('dv', [ +// 'title' => 'Mee dhivehi title eh', +// 'slug' => 'mee-dhivehi-slug-eh', +// 'body' => 'Mee dhivehi liyumeh', +// ]); +// +// $translation->save(); - $translation->save(); + $post->addTranslation('dv', 'title', 'Mee dhivehi title eh'); $this->assertEquals('Mee dhivehi title eh', $post->title_dv); } + /** @test */ + public function it_can_add_new_translation_locales_via_setter() + { + $post = Post::factory()->withAuthor()->create([ + 'lang' => 'en', + ]); + + $post->title_dv = 'Mee dhivehi title eh'; + + // get via locale because assertEquals complains that it'll always be true with title_dv since I just set it + $tmp = app()->getLocale(); + app()->setLocale('dv'); + $this->assertEquals('Mee dhivehi title eh', $post->title); + app()->setLocale($tmp); + } + + /** @test */ + public function it_can_add_new_translation_to_default_translation() + { + $post = Post::factory()->withAuthor()->create([ + 'lang' => 'en', + ]); + + $post->title_en = 'This is an English title'; + + app()->setLocale('en'); + $this->assertEquals('This is an English title', $post->title); + } + /** @test */ public function it_cannot_add_translation_locales_that_are_not_allowed() { @@ -493,10 +526,12 @@ public function it_cannot_add_translation_locales_that_are_not_allowed() 'lang' => 'en', ]); - $this->assertThrows($post->addTranslation('zh-CN', [ - 'title' => '这是一个中文标题', - 'slug' => '这是一只中国蛞蝓', - 'body' => '这是一个中国人的身体', - ]), LanguageNotAllowedException::class, 'zh-CN language not allowed'); +// $this->assertThrows($post->addTranslation('zh-CN', [ +// 'title' => '这是一个中文标题', +// 'slug' => '这是一只中国蛞蝓', +// 'body' => '这是一个中国人的身体', +// ]), LanguageNotAllowedException::class, 'zh-CN language not allowed'); + + $this->assertThrows($post->addTranslation('zh-CN', 'title', '这是一个中文标题'), LanguageNotAllowedException::class); } } diff --git a/tests/Unit/JsonTranslatable/IsJsonTranslatableTest.php b/tests/Unit/JsonTranslatable/IsJsonTranslatableTest.php index 7f4f1df..ebab5f7 100644 --- a/tests/Unit/JsonTranslatable/IsJsonTranslatableTest.php +++ b/tests/Unit/JsonTranslatable/IsJsonTranslatableTest.php @@ -355,24 +355,44 @@ public function it_can_check_if_is_default_translation_locale() $this->assertTrue($article->isDefaultTranslationLocale('en')); } - /** @test */ + /** @test + * @throws LanguageNotAllowedException + */ public function it_can_add_new_translation_locales() { $article = Article::factory()->withAuthor()->create([ 'lang' => 'en', ]); - $translation = $article->addTranslation('dv', [ - 'title' => 'Mee dhivehi title eh', - 'slug' => 'mee-dhivehi-slug-eh', - 'body' => 'Mee dhivehi liyumeh', - ]); +// $translation = $article->addTranslation('dv', [ +// 'title' => 'Mee dhivehi title eh', +// 'slug' => 'mee-dhivehi-slug-eh', +// 'body' => 'Mee dhivehi liyumeh', +// ]); +// +// $translation->save(); - $translation->save(); + $article->addTranslation('dv', 'title', 'Mee dhivehi title eh'); $this->assertEquals('Mee dhivehi title eh', $article->title_dv); } + /** @test */ + public function it_can_add_new_translation_locales_via_setter() + { + $article = Article::factory()->withAuthor()->create([ + 'lang' => 'en', + ]); + + $article->title_dv = 'Mee dhivehi title eh'; + + // get via locale because assertEquals complains that it'll always be true with title_dv since I just set it + $tmp = app()->getLocale(); + app()->setLocale('dv'); + $this->assertEquals('Mee dhivehi title eh', $article->title); + app()->setLocale($tmp); + } + /** @test */ public function it_cannot_add_translation_locales_that_are_not_allowed() { @@ -381,10 +401,12 @@ public function it_cannot_add_translation_locales_that_are_not_allowed() 'lang' => 'en', ]); - $this->assertThrows($article->addTranslation('zh-CN', [ - 'title' => '这是一个中文标题', - 'slug' => '这是一只中国蛞蝓', - 'body' => '这是一个中国人的身体', - ]), LanguageNotAllowedException::class, 'zh-CN language not allowed'); +// $this->assertThrows($article->addTranslation('zh-CN', [ +// 'title' => '这是一个中文标题', +// 'slug' => '这是一只中国蛞蝓', +// 'body' => '这是一个中国人的身体', +// ]), LanguageNotAllowedException::class, 'zh-CN language not allowed'); + + $this->assertThrows($article->addTranslation('zh-CN', 'title', '这是一个中文标题'), LanguageNotAllowedException::class); } } From 019855f207045bff71c399bf4d360640e6852aea Mon Sep 17 00:00:00 2001 From: WovenCoast Date: Mon, 24 Feb 2025 12:28:30 +0500 Subject: [PATCH 20/24] coverage: improve code coverage --- src/Abstract/IsTranslatable.php | 10 ++++- .../DbTranslatable/IsDbTranslatableTest.php | 36 ++++++++++++++++ .../IsJsonTranslatableTest.php | 42 ++++++++++++++++--- 3 files changed, 81 insertions(+), 7 deletions(-) diff --git a/src/Abstract/IsTranslatable.php b/src/Abstract/IsTranslatable.php index 9b66dca..527689d 100644 --- a/src/Abstract/IsTranslatable.php +++ b/src/Abstract/IsTranslatable.php @@ -103,10 +103,18 @@ public function isTranslatable(string $field): bool * @param string $locale * @param array $fields * @return $this + * @throws LanguageNotAllowedException */ public function addTranslations(string $locale, array $fields): static { - array_map([$this, 'addTranslation'], $fields); + if (! $this->isAllowedTranslationLocale($locale)) { + throw LanguageNotAllowedException::create($locale); + } + + foreach ($fields as $field => $value) { + $this->addTranslation($locale, $field, $value); + } + return $this; } diff --git a/tests/Unit/DbTranslatable/IsDbTranslatableTest.php b/tests/Unit/DbTranslatable/IsDbTranslatableTest.php index b3a89eb..b533f97 100644 --- a/tests/Unit/DbTranslatable/IsDbTranslatableTest.php +++ b/tests/Unit/DbTranslatable/IsDbTranslatableTest.php @@ -505,6 +505,42 @@ public function it_can_add_new_translation_locales_via_setter() app()->setLocale($tmp); } + /** @test + * @throws LanguageNotAllowedException + */ + public function it_can_add_translations_in_bulk() + { + $post = Post::factory()->withAuthor()->create([ + 'lang' => 'en', + ]); + + $post->addTranslations('dv', [ + 'title' => 'Mee dhivehi title eh', + 'slug' => 'mee-dhivehi-slug-eh', + 'body' => 'Mee dhivehi liyumeh', + ]); + + $this->assertEquals('Mee dhivehi title eh', $post->title_dv); + $this->assertEquals('Mee dhivehi liyumeh', $post->body_dv); + } + + /** @test + * @throws LanguageNotAllowedException + */ + public function it_cannot_add_translations_in_bulk_for_locales_that_are_not_allowed() + { + $this->expectException(LanguageNotAllowedException::class); + $post = Post::factory()->withAuthor()->create([ + 'lang' => 'en', + ]); + + $this->assertThrows($post->addTranslations('zh-CN', [ + 'title' => '这是一个中文标题', + 'slug' => '这是一只中国蛞蝓', + 'body' => '这是一个中国人的身体', + ]), LanguageNotAllowedException::class, 'zh-CN language not allowed'); + } + /** @test */ public function it_can_add_new_translation_to_default_translation() { diff --git a/tests/Unit/JsonTranslatable/IsJsonTranslatableTest.php b/tests/Unit/JsonTranslatable/IsJsonTranslatableTest.php index ebab5f7..2d18e76 100644 --- a/tests/Unit/JsonTranslatable/IsJsonTranslatableTest.php +++ b/tests/Unit/JsonTranslatable/IsJsonTranslatableTest.php @@ -393,6 +393,42 @@ public function it_can_add_new_translation_locales_via_setter() app()->setLocale($tmp); } + /** @test + * @throws LanguageNotAllowedException + */ + public function it_can_add_translations_in_bulk() + { + $article = Article::factory()->withAuthor()->create([ + 'lang' => 'en', + ]); + + $article->addTranslations('dv', [ + 'title' => 'Mee dhivehi title eh', + 'slug' => 'mee-dhivehi-slug-eh', + 'body' => 'Mee dhivehi liyumeh', + ]); + + $this->assertEquals('Mee dhivehi title eh', $article->title_dv); + $this->assertEquals('Mee dhivehi liyumeh', $article->body_dv); + } + + /** @test + * @throws LanguageNotAllowedException + */ + public function it_cannot_add_translations_in_bulk_for_locales_that_are_not_allowed() + { + $this->expectException(LanguageNotAllowedException::class); + $article = Article::factory()->withAuthor()->create([ + 'lang' => 'en', + ]); + + $this->assertThrows($article->addTranslations('zh-CN', [ + 'title' => '这是一个中文标题', + 'slug' => '这是一只中国蛞蝓', + 'body' => '这是一个中国人的身体', + ]), LanguageNotAllowedException::class, 'zh-CN language not allowed'); + } + /** @test */ public function it_cannot_add_translation_locales_that_are_not_allowed() { @@ -401,12 +437,6 @@ public function it_cannot_add_translation_locales_that_are_not_allowed() 'lang' => 'en', ]); -// $this->assertThrows($article->addTranslation('zh-CN', [ -// 'title' => '这是一个中文标题', -// 'slug' => '这是一只中国蛞蝓', -// 'body' => '这是一个中国人的身体', -// ]), LanguageNotAllowedException::class, 'zh-CN language not allowed'); - $this->assertThrows($article->addTranslation('zh-CN', 'title', '这是一个中文标题'), LanguageNotAllowedException::class); } } From 561b4af39aea317a922b22bc1b0c77d7d0cb5778 Mon Sep 17 00:00:00 2001 From: WovenCoast Date: Mon, 24 Feb 2025 12:41:04 +0500 Subject: [PATCH 21/24] coverage: fix compatibility issues with phpunit 9.5.10 --- .../Unit/DbTranslatable/IsDbTranslatableTest.php | 16 +++++++--------- .../JsonTranslatable/IsJsonTranslatableTest.php | 10 +++++++--- 2 files changed, 14 insertions(+), 12 deletions(-) diff --git a/tests/Unit/DbTranslatable/IsDbTranslatableTest.php b/tests/Unit/DbTranslatable/IsDbTranslatableTest.php index b533f97..4dd816f 100644 --- a/tests/Unit/DbTranslatable/IsDbTranslatableTest.php +++ b/tests/Unit/DbTranslatable/IsDbTranslatableTest.php @@ -530,15 +530,17 @@ public function it_can_add_translations_in_bulk() public function it_cannot_add_translations_in_bulk_for_locales_that_are_not_allowed() { $this->expectException(LanguageNotAllowedException::class); + $this->expectExceptionMessage('zh-CN language not allowed'); + $post = Post::factory()->withAuthor()->create([ 'lang' => 'en', ]); - $this->assertThrows($post->addTranslations('zh-CN', [ + $post->addTranslations('zh-CN', [ 'title' => '这是一个中文标题', 'slug' => '这是一只中国蛞蝓', 'body' => '这是一个中国人的身体', - ]), LanguageNotAllowedException::class, 'zh-CN language not allowed'); + ]); } /** @test */ @@ -558,16 +560,12 @@ public function it_can_add_new_translation_to_default_translation() public function it_cannot_add_translation_locales_that_are_not_allowed() { $this->expectException(LanguageNotAllowedException::class); + $this->expectExceptionMessage('zh-CN language not allowed'); + $post = Post::factory()->withAuthor()->create([ 'lang' => 'en', ]); -// $this->assertThrows($post->addTranslation('zh-CN', [ -// 'title' => '这是一个中文标题', -// 'slug' => '这是一只中国蛞蝓', -// 'body' => '这是一个中国人的身体', -// ]), LanguageNotAllowedException::class, 'zh-CN language not allowed'); - - $this->assertThrows($post->addTranslation('zh-CN', 'title', '这是一个中文标题'), LanguageNotAllowedException::class); + $post->addTranslation('zh-CN', 'title', '这是一个中文标题'); } } diff --git a/tests/Unit/JsonTranslatable/IsJsonTranslatableTest.php b/tests/Unit/JsonTranslatable/IsJsonTranslatableTest.php index 2d18e76..0747926 100644 --- a/tests/Unit/JsonTranslatable/IsJsonTranslatableTest.php +++ b/tests/Unit/JsonTranslatable/IsJsonTranslatableTest.php @@ -418,25 +418,29 @@ public function it_can_add_translations_in_bulk() public function it_cannot_add_translations_in_bulk_for_locales_that_are_not_allowed() { $this->expectException(LanguageNotAllowedException::class); + $this->expectExceptionMessage('zh-CN language not allowed'); + $article = Article::factory()->withAuthor()->create([ 'lang' => 'en', ]); - $this->assertThrows($article->addTranslations('zh-CN', [ + $article->addTranslations('zh-CN', [ 'title' => '这是一个中文标题', 'slug' => '这是一只中国蛞蝓', 'body' => '这是一个中国人的身体', - ]), LanguageNotAllowedException::class, 'zh-CN language not allowed'); + ]); } /** @test */ public function it_cannot_add_translation_locales_that_are_not_allowed() { $this->expectException(LanguageNotAllowedException::class); + $this->expectExceptionMessage('zh-CN language not allowed'); + $article = Article::factory()->withAuthor()->create([ 'lang' => 'en', ]); - $this->assertThrows($article->addTranslation('zh-CN', 'title', '这是一个中文标题'), LanguageNotAllowedException::class); + $article->addTranslation('zh-CN', 'title', '这是一个中文标题'); } } From d4f0c31c0c55c124d79145c2bade11e7017bc96e Mon Sep 17 00:00:00 2001 From: Github Actions Bot <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 24 Feb 2025 07:41:48 +0000 Subject: [PATCH 22/24] Update code coverage badge --- clover.xml | 317 ++++++++++++++++++++++++++++++----------------------- 1 file changed, 177 insertions(+), 140 deletions(-) diff --git a/clover.xml b/clover.xml index 5b5236b..b77f5c4 100644 --- a/clover.xml +++ b/clover.xml @@ -1,106 +1,119 @@ - - + + - + - - - - - - - + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - + + + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - + + + - - - - + + + + - + - + - + @@ -108,92 +121,116 @@ - - - - + + + - - - + + + - - - + + + + + + + + + + + + + + + + + + + + + + - + - - + - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + - - + + - - + + - - + + - - - - - - - - + + + + + + + + + + + + + + + - - - + + + - + - - - - - - - - + + + + + + + + - + From f1911c423ba9ad3da5df72da04b68c0a397a9fa8 Mon Sep 17 00:00:00 2001 From: WovenCoast Date: Mon, 24 Feb 2025 16:29:27 +0500 Subject: [PATCH 23/24] docs: update docs --- README.md | 47 +++++++++++ docs/about-us.md | 2 +- docs/advanced-usage/_category_.json | 11 +++ .../10-setting-up-your-migration-and-model.md | 83 +++++++++++++++++++ docs/basic-usage/20-fetching-translations.md | 30 +++++++ docs/basic-usage/30-adding-translations.md | 22 +++++ ...nce-isdbtranslatable-isjsontranslatable.md | 14 ++++ docs/basic-usage/how-to-use-feature.md | 5 -- docs/changelog.md | 2 +- docs/installation-and-setup.md | 27 +++++- docs/intro.md | 60 +++++++++++++- docs/questions-and-security.md | 2 +- src/DbTranslatable/DbTranslatableSchema.php | 9 ++ 13 files changed, 304 insertions(+), 10 deletions(-) create mode 100644 docs/advanced-usage/_category_.json create mode 100644 docs/basic-usage/10-setting-up-your-migration-and-model.md create mode 100644 docs/basic-usage/20-fetching-translations.md create mode 100644 docs/basic-usage/30-adding-translations.md create mode 100644 docs/basic-usage/70-difference-isdbtranslatable-isjsontranslatable.md delete mode 100644 docs/basic-usage/how-to-use-feature.md diff --git a/README.md b/README.md index 3a58807..6eeba53 100644 --- a/README.md +++ b/README.md @@ -7,8 +7,55 @@ ## Introduction + Adds multi-lingual to Laravel models +To get started with this package, you can simply add `DbTranslatableSchema::columns($table);` or `JsonTranslatableSchema::columns($table);` to your migration `up` function. + +```php +use Illuminate\Database\Migrations\Migration; +use Illuminate\Database\Schema\Blueprint; +use Illuminate\Support\Facades\Schema; +use Javaabu\Translatable\DbTranslatable\DbTranslatableSchema; + +return new class extends Migration { + public function up(): void + { + Schema::create('posts', function (Blueprint $table) { + $table->id(); + + ... + + DbTranslatableSchema::columns($table); + }); + } + + public function down(): void + { + Schema::dropIfExists('posts'); + } +}; +``` + +And then all you need to do is add the `Translatable` implementation using the `IsDbTranslatable` or `IsJsonTranslatable` trait. + +```php +... + +class Post extends Model implements Translatable +{ + use IsDbTranslatable; + +... +``` + +Now, your models will automatically be translated according to the current `app()->getLocale()`. To add different translations, all you need to do is + +```php +// to add title for dv language +$post->title_dv = "Mee Dhivehi title eh"; +``` + ## Documentation You'll find the documentation on [https://docs.javaabu.com/docs/translatable](https://docs.javaabu.com/docs/translatable). diff --git a/docs/about-us.md b/docs/about-us.md index a5951ce..75dfd0e 100644 --- a/docs/about-us.md +++ b/docs/about-us.md @@ -1,6 +1,6 @@ --- title: About Us -sidebar_position: 1.6 +sidebar_position: 1.7 --- [Javaabu](https://javaabu.com) is a web design agency based in the Maldives. diff --git a/docs/advanced-usage/_category_.json b/docs/advanced-usage/_category_.json new file mode 100644 index 0000000..074bf15 --- /dev/null +++ b/docs/advanced-usage/_category_.json @@ -0,0 +1,11 @@ +{ + "position": 1.4, + "label": "Advanced Usage", + "collapsible": true, + "collapsed": false, + "link": { + "type": "generated-index", + "slug": "/translatable/_categories/advanced-usage", + "description": "Additional features available in this package." + } +} diff --git a/docs/basic-usage/10-setting-up-your-migration-and-model.md b/docs/basic-usage/10-setting-up-your-migration-and-model.md new file mode 100644 index 0000000..a3bb271 --- /dev/null +++ b/docs/basic-usage/10-setting-up-your-migration-and-model.md @@ -0,0 +1,83 @@ +--- +title: Set up your Migration and Model +sidebar_position: 10 +--- + +:::info + +Translatables currently provides **two** different types of translatables, `Db` and `Json`. Check out [Advanced Usage > Difference between DB and JSON translatable](./70-difference-isdbtranslatable-isjsontranslatable) to learn the differences and design considerations for both + +::: + +## Setting up your migrations + +If you are setting up a new model, you can simply add either `DbTranslatableSchema::columns($table);` or `JsonTranslatableSchema::columns($table);` into your migration schema create function. + +```php +use Javaabu\Translatable\DbTranslatable\DbTranslatableSchema; + +return new class extends Migration { + public function up(): void + { + Schema::create('posts', function (Blueprint $table) { + $table->id(); + + // ... + + DbTranslatableSchema::columns($table); + }); + } + + public function down(): void + { + Schema::dropIfExists('posts'); + } +}; +``` + +Or if you have already made the model, you can write a migration to add the columns to the existing table. + +```php +use Javaabu\Translatable\DbTranslatable\DbTranslatableSchema; + +return new class extends Migration { + public function up(): void + { + Schema::create('posts', function (Blueprint $table) { + DbTranslatableSchema::columns($table); + }); + } + + public function down(): void + { + Schema::table('posts', function (Blueprint $table) { + DbTranslatableSchema::revert($table); + }); + } +}; +``` + +:::danger + +The revert function is **unstable** as the testing suite for this has not been written yet. Use it with caution. + +::: + +## Setting up your models + + +All you need to do is add the `Translatable` implementation using the `IsDbTranslatable` or `IsJsonTranslatable` trait. + +```php +... +use Javaabu\Translatable\DbTranslatable\IsDbTranslatable; +use Javaabu\Translatable\Translatable; + +class Post extends Model implements Translatable +{ + use IsDbTranslatable; + +... +``` + +Once this is setup, you are good to go! diff --git a/docs/basic-usage/20-fetching-translations.md b/docs/basic-usage/20-fetching-translations.md new file mode 100644 index 0000000..f095fe8 --- /dev/null +++ b/docs/basic-usage/20-fetching-translations.md @@ -0,0 +1,30 @@ +--- +title: Fetching Translations +sidebar_position: 20 +--- + +There are multiple ways to use locale translated attributes within your code. + +## Using application locale + +If you use `app()->setLocale('dv');` then translatables will automatically detect this and give you the translated attribute if available. + +```php +app()->setLocale('dv'); + +// ... + +// assuming you have added the translation +$post->title // Mee dhivehi title eh +``` + +## Using language code suffixes + +If you need a specific locale, you can use the language code suffix to always get that locale. + +```php +$post->title_dv // Mee dhivehi title eh +$post->title_ar // null +``` + +By default, the language code suffix does not fallback to default locale. To change this behaviour, change the `lang_suffix_should_fallback` to true in the [config](../installation-and-setup.md#publishing-the-config-file) file. diff --git a/docs/basic-usage/30-adding-translations.md b/docs/basic-usage/30-adding-translations.md new file mode 100644 index 0000000..058ba10 --- /dev/null +++ b/docs/basic-usage/30-adding-translations.md @@ -0,0 +1,22 @@ +--- +title: Adding Translations +sidebar_position: 30 +--- + +Translations can be added using the `addTranslation` / `addTranslations` methods available on models with Translatables. + +```php +// add translation for one attribute +$post->addTranslation('dv', 'title', 'Mee dhivehi title eh'); +$post->addTranslation('dv', 'body', 'Mee dhivehi liyumeh'); + +// add translations for multiple attributes at once +$post->addTranslations('dv', [ + 'title' => 'Mee dhivehi title eh', + 'body' => 'Mee dhivehi liyumeh' +]); + +$post->title_dv // Mee dhivehi title eh +$post->body_dv // Mee dhivehi liyumeh +``` + diff --git a/docs/basic-usage/70-difference-isdbtranslatable-isjsontranslatable.md b/docs/basic-usage/70-difference-isdbtranslatable-isjsontranslatable.md new file mode 100644 index 0000000..bca0354 --- /dev/null +++ b/docs/basic-usage/70-difference-isdbtranslatable-isjsontranslatable.md @@ -0,0 +1,14 @@ +--- +title: Difference between DB and JSON translatable +sidebar_position: 70 +--- + +Functionally, both `IsDbTranslatable` and `IsJsonTranslatable` both implement all functions of `Translatable`, there are no differences in syntax when using either of these. + +But there are a few notable differences in how each type implements `Translatables`. Depending on the use case and scale of the application, one may make more sense than the other. + +| IsDbTranslatable | IsJsonTranslatable | +|---------------------------------------------------------|-------------------------------------------------------------| +| - Works by creating additional rows for each language | - Works by adding a `translatables` JSON field to every row | +| - Additional queries required to check if locale exists | - No additional queries needed for translating | +| - Indexable by default | - Needs to search through JSON field | diff --git a/docs/basic-usage/how-to-use-feature.md b/docs/basic-usage/how-to-use-feature.md deleted file mode 100644 index eb46af9..0000000 --- a/docs/basic-usage/how-to-use-feature.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: How to use some feature ---- - -Write the name of the feature as the title and give instructions on how to use it diff --git a/docs/changelog.md b/docs/changelog.md index 9f959f6..8bc8a46 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,6 +1,6 @@ --- title: Changelog -sidebar_position: 1.5 +sidebar_position: 1.6 --- All notable changes to this package are documented on [GitHub](https://github.com/Javaabu/translatable/blob/main/CHANGELOG.md) diff --git a/docs/installation-and-setup.md b/docs/installation-and-setup.md index 8eb1cd2..de38306 100644 --- a/docs/installation-and-setup.md +++ b/docs/installation-and-setup.md @@ -20,5 +20,30 @@ php artisan vendor:publish --provider="Javaabu\Translatable\TranslatableServiceP This is the default content of the config file: ```php -// TODO +return [ + /* + |-------------------------------------------------------------------------- + | Some config option + |-------------------------------------------------------------------------- + | + | Give a description of what each config option is like this + | + */ + + 'fields_ignored_for_translation' => [ + 'id', + 'lang', + 'created_at', + 'updated_at', + 'deleted_at', + ], + 'allowed_translation_locales' => [ + 'en' => 'English', + 'dv' => 'Dhivehi', + 'jp' => 'Japanese', + ], + 'lang_suffix_should_fallback' => false, +]; + + ``` diff --git a/docs/intro.md b/docs/intro.md index 25e0bf7..05531db 100644 --- a/docs/intro.md +++ b/docs/intro.md @@ -11,4 +11,62 @@ This package is currently under development. If anything works, that's a surpris ::: -[Translatable](https://github.com/Javaabu/translatable) Adds multi-lingual to Laravel models. +[Translatable](https://github.com/Javaabu/translatable) adds multi-lingual to Laravel models. + +To get started with this package, you can simply add `DbTranslatableSchema::columns($table);` to your migration `up` function. + +```php +use Illuminate\Database\Migrations\Migration; +use Illuminate\Database\Schema\Blueprint; +use Illuminate\Support\Facades\Schema; +use Javaabu\Translatable\DbTranslatable\DbTranslatableSchema; + +return new class extends Migration { + public function up(): void + { + Schema::create('posts', function (Blueprint $table) { + $table->id(); + + ... + + DbTranslatableSchema::columns($table); + }); + } + + public function down(): void + { + Schema::dropIfExists('posts'); + } +}; +``` + +:::danger + +This function is not currently implemented but is in the plans at this moment. + +> We also provide a `DBTranslatableSchema::revert($table)` function to put in the `down` function, this isn't necessary but it's good to have. + +::: + +And then all you need to do is add the `Translatable` implementation using the `IsDbTranslatable` or `IsJsonTranslatable` trait. + +```php +... + +class Post extends Model implements Translatable +{ + use IsDbTranslatable; + +... +``` + +> Differences between `IsDbTranslatable` and `IsJsonTranslatable` are listed in [] + +Now, your models will automatically be translated according to the current `app()->getLocale()`. To add different translations, all you need to do is + +```php +// to add title for dv language +$post->title_dv = "Mee Dhivehi title eh"; +``` + +> If adding translations give an error, make sure the locale is allowed in `allowed_translation_locales` in `config/translatable.php`. Check out [Installation and Setup > Publishing the config file](./installation-and-setup.md#publishing-the-config-file) for information on how to setup your config file. diff --git a/docs/questions-and-security.md b/docs/questions-and-security.md index 375937c..c48c95f 100644 --- a/docs/questions-and-security.md +++ b/docs/questions-and-security.md @@ -1,6 +1,6 @@ --- title: Questions and Security -sidebar_position: 1.4 +sidebar_position: 1.5 --- Find yourself stuck using the package? Found a bug? Do you have general questions or suggestions for improving this package? Feel free to create an [issue](../../issues) on GitHub, we'll try to address it as soon as possible. diff --git a/src/DbTranslatable/DbTranslatableSchema.php b/src/DbTranslatable/DbTranslatableSchema.php index ebf56a0..b3aeadd 100644 --- a/src/DbTranslatable/DbTranslatableSchema.php +++ b/src/DbTranslatable/DbTranslatableSchema.php @@ -12,4 +12,13 @@ public static function columns(Blueprint $table): void $table->string('lang')->index(); } + + public static function revert(Blueprint $table): void + { + $table->dropForeign('translatable_parent_id'); + $table->dropColumn('translatable_parent_id'); + + $table->dropIndex('lang'); + $table->dropColumn('lang'); + } } From 279689b5dd75cf920f11543f61827f51d4c496bb Mon Sep 17 00:00:00 2001 From: WovenCoast Date: Mon, 24 Feb 2025 16:29:51 +0500 Subject: [PATCH 24/24] feat: add config option for lang suffix fallback --- config/translatable.php | 3 ++- src/Abstract/IsTranslatable.php | 10 ++++------ src/JsonTranslatable/JsonTranslatableSchema.php | 6 ++++++ 3 files changed, 12 insertions(+), 7 deletions(-) diff --git a/config/translatable.php b/config/translatable.php index 474fe8b..bf10973 100644 --- a/config/translatable.php +++ b/config/translatable.php @@ -21,5 +21,6 @@ 'en' => 'English', 'dv' => 'Dhivehi', 'jp' => 'Japanese', - ] + ], + 'lang_suffix_should_fallback' => false, ]; diff --git a/src/Abstract/IsTranslatable.php b/src/Abstract/IsTranslatable.php index 527689d..0e25151 100644 --- a/src/Abstract/IsTranslatable.php +++ b/src/Abstract/IsTranslatable.php @@ -150,13 +150,13 @@ public function getAttribute($key): mixed // translate using current app locale if possible if ($this->isTranslatable($key)) { - return $this->translate($key, app()->currentLocale(), false); + return $this->translate($key, app()->currentLocale()); } // check if is a suffixed attribute [$field, $locale] = $this->getFieldAndLocale($key); if ($locale && $this->isTranslatable($field)) { - return $this->translate($field, $locale, false); + return $this->translate($field, $locale, config('translatable.lang_suffix_should_fallback', false)); } // fallback to parent @@ -170,10 +170,8 @@ public function setAttribute($key, $value): mixed { [$field, $locale] = $this->getFieldAndLocale($key); - if (! $locale) return parent::setAttribute($key, $value); - - if ($this->isTranslatable($field)) { - $this->addTranslation($locale, $field, $value); + if ($locale && $this->isTranslatable($field)) { + return $this->addTranslation($locale, $field, $value); } return parent::setAttribute($key, $value); diff --git a/src/JsonTranslatable/JsonTranslatableSchema.php b/src/JsonTranslatable/JsonTranslatableSchema.php index 53fb4ce..6cc808d 100644 --- a/src/JsonTranslatable/JsonTranslatableSchema.php +++ b/src/JsonTranslatable/JsonTranslatableSchema.php @@ -22,4 +22,10 @@ public static function columns(Blueprint $table): void $table->string('lang')->index(); } + + public static function revert(Blueprint $table): void + { + $table->dropIndex('lang'); + $table->dropColumn(['translations', 'lang']); + } }