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/coverage.svg b/.github/coverage.svg
new file mode 100644
index 0000000..1db4533
--- /dev/null
+++ b/.github/coverage.svg
@@ -0,0 +1,20 @@
+
\ No newline at end of file
diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml
index 7918815..2860d21 100644
--- a/.github/workflows/run-tests.yml
+++ b/.github/workflows/run-tests.yml
@@ -1,60 +1,70 @@
name: tests
on:
- pull_request:
- branches:
- - main
- push:
- branches:
- - main
+ 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') && matrix.php == '8.3' && matrix.laravel == '11.*' && matrix.stability == 'prefer-stable' && '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') && 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') && 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
+ push_badge: true
+ repo_token: ${{ secrets.GITHUB_TOKEN }}
diff --git a/README.md b/README.md
index 1ea42a8..6eeba53 100644
--- a/README.md
+++ b/README.md
@@ -3,12 +3,59 @@
[](https://packagist.org/packages/javaabu/translatable)
[](../../actions/workflows/run-tests.yml)
[](https://packagist.org/packages/javaabu/translatable)
-
+
## 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).
@@ -43,6 +90,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
diff --git a/clover.xml b/clover.xml
new file mode 100644
index 0000000..b77f5c4
--- /dev/null
+++ b/clover.xml
@@ -0,0 +1,236 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/composer.json b/composer.json
index e7861b3..4883b78 100644
--- a/composer.json
+++ b/composer.json
@@ -13,6 +13,11 @@
"name": "Xylam",
"email": "jailam@javaabu.com",
"role": "Developer"
+ },
+ {
+ "name": "FlameXode",
+ "email": "hi@flamexo.de",
+ "role": "Developer"
}
],
"require": {
diff --git a/config/translatable.php b/config/translatable.php
index ea4a711..bf10973 100644
--- a/config/translatable.php
+++ b/config/translatable.php
@@ -10,5 +10,17 @@
|
*/
- // TODO
+ '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/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/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/src/Abstract/IsTranslatable.php b/src/Abstract/IsTranslatable.php
new file mode 100644
index 0000000..0e25151
--- /dev/null
+++ b/src/Abstract/IsTranslatable.php
@@ -0,0 +1,179 @@
+getTable());
+ }
+
+ /**
+ * 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 = $this->getAllAttributes();
+
+ $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());
+ }
+
+ /**
+ * Bulk add translatable fields
+ *
+ * @param string $locale
+ * @param array $fields
+ * @return $this
+ * @throws LanguageNotAllowedException
+ */
+ public function addTranslations(string $locale, array $fields): static
+ {
+ if (! $this->isAllowedTranslationLocale($locale)) {
+ throw LanguageNotAllowedException::create($locale);
+ }
+
+ foreach ($fields as $field => $value) {
+ $this->addTranslation($locale, $field, $value);
+ }
+
+ return $this;
+ }
+
+ /**
+ * 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());
+ }
+
+ // check if is a suffixed attribute
+ [$field, $locale] = $this->getFieldAndLocale($key);
+ if ($locale && $this->isTranslatable($field)) {
+ return $this->translate($field, $locale, config('translatable.lang_suffix_should_fallback', false));
+ }
+
+ // fallback to parent
+ return parent::getAttribute($key);
+ }
+
+ /**
+ * @throws LanguageNotAllowedException
+ */
+ public function setAttribute($key, $value): mixed
+ {
+ [$field, $locale] = $this->getFieldAndLocale($key);
+
+ if ($locale && $this->isTranslatable($field)) {
+ return $this->addTranslation($locale, $field, $value);
+ }
+
+ return parent::setAttribute($key, $value);
+ }
+}
diff --git a/src/DbTranslatable/DbTranslatableSchema.php b/src/DbTranslatable/DbTranslatableSchema.php
new file mode 100644
index 0000000..b3aeadd
--- /dev/null
+++ b/src/DbTranslatable/DbTranslatableSchema.php
@@ -0,0 +1,24 @@
+foreignId('translatable_parent_id')->nullable();
+
+ $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');
+ }
+}
diff --git a/src/DbTranslatable/IsDbTranslatable.php b/src/DbTranslatable/IsDbTranslatable.php
new file mode 100644
index 0000000..fcec085
--- /dev/null
+++ b/src/DbTranslatable/IsDbTranslatable.php
@@ -0,0 +1,170 @@
+fields_ignored_for_translation,
+ config('translatable.fields_ignored_for_translation')
+ )));
+ }
+
+ public function translations()
+ {
+ return $this->hasMany(self::class, 'translatable_parent_id', 'id');
+ }
+
+ public function defaultTranslation()
+ {
+ return $this->belongsTo(self::class, 'translatable_parent_id', '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;
+ }
+
+ // 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 in any of the translated rows
+ 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 (! $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) {
+ $this->delete();
+ } else {
+ $defaultTranslation = $this->isDefaultTranslation() ? $this : $this->defaultTranslation;
+ if ($defaultTranslation->lang == $locale) {
+ $defaultTranslation->delete();
+ }
+ $defaultTranslation->translations()->where('lang', $locale)->delete();
+ }
+ }
+ }
+
+ 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();
+ }
+ }
+
+ /**
+ * @throws LanguageNotAllowedException
+ */
+ public function addTranslation(string $locale, string $field, string $value): static
+ {
+ if (! $this->isAllowedTranslationLocale($locale)) {
+ throw LanguageNotAllowedException::create($locale);
+ }
+
+ $defaultTranslation = $this->isDefaultTranslation() ? $this : $this->defaultTranslation;
+
+ // check if the default translation is already the correct locale
+ if ($defaultTranslation->lang == $locale) {
+ $defaultTranslation->setAttribute($field, $value);
+ $defaultTranslation->save();
+ return $defaultTranslation;
+ }
+
+ // 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($field, $value);
+ $newTranslation->setAttribute('lang', $locale);
+ $newTranslation->save();
+
+ return $newTranslation;
+ }
+}
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 @@
+fields_ignored_for_translation,
+ config('translatable.fields_ignored_for_translation')
+ )));
+ }
+
+ /**
+ * 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 in the translatable fields list 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];
+ }
+
+ /**
+ * Gets default translation locale
+ *
+ * @return string
+ */
+ public function getDefaultTranslationLocale(): string
+ {
+ return $this->getAttributeValue('lang');
+ }
+
+ /**
+ * Clear translations for a given language or for all languages if none is given
+ *
+ * @param string|null $locale
+ * @return void
+ */
+ 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]);
+ }
+
+
+ /**
+ * Add a new locale to this object
+ *
+ * @param string $locale
+ * @param string $field
+ * @param string $value
+ * @return $this
+ */
+ public function addTranslation(string $locale, string $field, string $value): static
+ {
+ if (! $this->isAllowedTranslationLocale($locale)) {
+ throw LanguageNotAllowedException::create($locale);
+ }
+
+ /** @var array $translations */
+ $translations = $this->translations ?? [];
+ $translations[$locale] = array_merge(
+ array_key_exists($locale, $translations) ? $translations[$locale] : [],
+ [$field => $value],
+ ['lang' => $locale]
+ );
+ $this->translations = $translations;
+ $this->save();
+
+ return $this;
+ }
+}
diff --git a/src/JsonTranslatable/JsonTranslatableSchema.php b/src/JsonTranslatable/JsonTranslatableSchema.php
new file mode 100644
index 0000000..6cc808d
--- /dev/null
+++ b/src/JsonTranslatable/JsonTranslatableSchema.php
@@ -0,0 +1,31 @@
+runningUnitTests()) {
+// $table->text('translations')->nullable();
+// } else {
+ $table->json('translations')->nullable();
+// }
+
+ $table->string('lang')->index();
+ }
+
+ public static function revert(Blueprint $table): void
+ {
+ $table->dropIndex('lang');
+ $table->dropColumn(['translations', 'lang']);
+ }
+}
diff --git a/src/Translatable.php b/src/Translatable.php
new file mode 100644
index 0000000..58d9af6
--- /dev/null
+++ b/src/Translatable.php
@@ -0,0 +1,140 @@
+
+ */
+ public function getAllowedTranslationLocales(): array;
+
+ /**
+ * Check if given locale is allowed
+ *
+ * @param string $locale
+ * @return boolean
+ */
+ 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
+ *
+ * @param string $locale
+ * @param array $fields
+ * @return $this
+ */
+ public function addTranslations(string $locale, array $fields): static;
+}
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/Factories/ArticleFactory.php b/tests/TestSupport/Factories/ArticleFactory.php
new file mode 100644
index 0000000..a4d7832
--- /dev/null
+++ b/tests/TestSupport/Factories/ArticleFactory.php
@@ -0,0 +1,34 @@
+ $this->faker->title(),
+ 'slug' => $this->faker->slug(),
+ 'body' => $this->faker->paragraph(5),
+ 'lang' => $this->faker->locale(),
+ 'created_at' => Carbon::now(),
+ 'updated_at' => Carbon::now(),
+ ];
+ }
+
+ 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/Factories/PostFactory.php b/tests/TestSupport/Factories/PostFactory.php
new file mode 100644
index 0000000..69c0f59
--- /dev/null
+++ b/tests/TestSupport/Factories/PostFactory.php
@@ -0,0 +1,34 @@
+ $this->faker->sentence(),
+ 'slug' => $this->faker->slug(),
+ 'body' => $this->faker->paragraph(5),
+ 'lang' => $this->faker->locale(),
+ 'created_at' => Carbon::now(),
+ 'updated_at' => Carbon::now(),
+ ];
+ }
+
+ 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
new file mode 100644
index 0000000..26e424d
--- /dev/null
+++ b/tests/TestSupport/Models/Article.php
@@ -0,0 +1,45 @@
+ 'array',
+ ];
+
+ protected static function newFactory(): ArticleFactory
+ {
+ return new ArticleFactory();
+ }
+
+ public function getTranslatables(): array
+ {
+ return [
+ 'title',
+ 'body',
+ ];
+ }
+
+ public function getNonTranslatablePivots(): array
+ {
+ return [
+ 'author_id'
+ ];
+ }
+}
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 @@
+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_16_140456_create_articles_table.php b/tests/TestSupport/database/2025_02_16_140456_create_articles_table.php
new file mode 100644
index 0000000..c544f84
--- /dev/null
+++ b/tests/TestSupport/database/2025_02_16_140456_create_articles_table.php
@@ -0,0 +1,29 @@
+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/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..4dd816f
--- /dev/null
+++ b/tests/Unit/DbTranslatable/IsDbTranslatableTest.php
@@ -0,0 +1,571 @@
+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',
+ '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'));
+ $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 */
+ 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',
+ 'translatable_parent_id' => $post->id,
+ ]);
+
+ $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',
+ '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);
+ $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',
+ '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();
+ 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_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',
+ '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();
+ 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()
+ {
+ $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',
+ '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'));
+ }
+
+ /** @test */
+ public function it_can_clear_translations_for_one_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('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_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()
+ {
+ $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',
+ '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();
+
+ $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([
+ '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,
+ ]);
+
+ $this->assertFalse($post->hasTranslation('fr'));
+ $this->assertTrue($post_jp->hasTranslation('dv'));
+ $this->assertTrue($post->hasTranslation('en'));
+ $tmp = app()->getLocale();
+ app()->setLocale('dv');
+ $this->assertTrue($post->hasTranslation());
+ app()->setLocale($tmp);
+ }
+
+ /** @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',
+ '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'));
+ $this->assertTrue($post->isDefaultTranslationLocale('en'));
+ }
+
+ /** @test
+ * @throws LanguageNotAllowedException
+ */
+ public function it_can_add_new_translation_locales()
+ {
+ $post = Post::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();
+
+ $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
+ * @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);
+ $this->expectExceptionMessage('zh-CN language not allowed');
+
+ $post = Post::factory()->withAuthor()->create([
+ 'lang' => 'en',
+ ]);
+
+ $post->addTranslations('zh-CN', [
+ 'title' => '这是一个中文标题',
+ 'slug' => '这是一只中国蛞蝓',
+ 'body' => '这是一个中国人的身体',
+ ]);
+ }
+
+ /** @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()
+ {
+ $this->expectException(LanguageNotAllowedException::class);
+ $this->expectExceptionMessage('zh-CN language not allowed');
+
+ $post = Post::factory()->withAuthor()->create([
+ 'lang' => 'en',
+ ]);
+
+ $post->addTranslation('zh-CN', 'title', '这是一个中文标题');
+ }
+}
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..0747926
--- /dev/null
+++ b/tests/Unit/JsonTranslatable/IsJsonTranslatableTest.php
@@ -0,0 +1,446 @@
+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_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()
+ {
+ $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->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 */
+ 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',
+ ]
+ ]
+ ]);
+
+ // 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
+ $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'));
+ $tmp = app()->getLocale();
+ app()->setLocale('dv');
+ $this->assertTrue($article->hasTranslation());
+ app()->setLocale($tmp);
+ }
+
+ /** @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
+ * @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->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
+ * @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);
+ $this->expectExceptionMessage('zh-CN language not allowed');
+
+ $article = Article::factory()->withAuthor()->create([
+ 'lang' => 'en',
+ ]);
+
+ $article->addTranslations('zh-CN', [
+ 'title' => '这是一个中文标题',
+ 'slug' => '这是一只中国蛞蝓',
+ 'body' => '这是一个中国人的身体',
+ ]);
+ }
+
+ /** @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',
+ ]);
+
+ $article->addTranslation('zh-CN', 'title', '这是一个中文标题');
+ }
+}