├── .editorconfig ├── .github ├── FUNDING.yml ├── dependabot.yml └── workflows │ ├── dependabot-auto-merge.yml │ ├── php-cs-fixer.yml │ ├── run-tests.yml │ └── update-changelog.yml ├── .phpunit.cache └── test-results ├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── composer.json └── src ├── Events └── TranslationHasBeenSetEvent.php ├── Exceptions └── AttributeIsNotTranslatable.php ├── Facades └── Translatable.php ├── HasTranslations.php ├── Translatable.php └── TranslatableServiceProvider.php /.editorconfig: -------------------------------------------------------------------------------- 1 | ; This file is for unifying the coding style for different editors and IDEs. 2 | ; More information at http://editorconfig.org 3 | 4 | root = true 5 | 6 | [*] 7 | charset = utf-8 8 | indent_size = 4 9 | indent_style = space 10 | end_of_line = lf 11 | insert_final_newline = true 12 | trim_trailing_whitespace = true 13 | 14 | [*.md] 15 | trim_trailing_whitespace = false 16 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: spatie 2 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | updates: 4 | 5 | - package-ecosystem: "github-actions" 6 | directory: "/" 7 | schedule: 8 | interval: "weekly" 9 | -------------------------------------------------------------------------------- /.github/workflows/dependabot-auto-merge.yml: -------------------------------------------------------------------------------- 1 | name: dependabot-auto-merge 2 | on: pull_request_target 3 | 4 | permissions: 5 | pull-requests: write 6 | contents: write 7 | 8 | jobs: 9 | dependabot: 10 | runs-on: ubuntu-latest 11 | if: ${{ github.actor == 'dependabot[bot]' }} 12 | steps: 13 | 14 | - name: Dependabot metadata 15 | id: metadata 16 | uses: dependabot/fetch-metadata@v2.4.0 17 | with: 18 | github-token: "${{ secrets.GITHUB_TOKEN }}" 19 | compat-lookup: true 20 | 21 | - name: Auto-merge Dependabot PRs for semver-minor updates 22 | if: ${{steps.metadata.outputs.update-type == 'version-update:semver-minor'}} 23 | run: gh pr merge --auto --merge "$PR_URL" 24 | env: 25 | PR_URL: ${{github.event.pull_request.html_url}} 26 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} 27 | 28 | - name: Auto-merge Dependabot PRs for semver-patch updates 29 | if: ${{steps.metadata.outputs.update-type == 'version-update:semver-patch'}} 30 | run: gh pr merge --auto --merge "$PR_URL" 31 | env: 32 | PR_URL: ${{github.event.pull_request.html_url}} 33 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} 34 | 35 | - name: Auto-merge Dependabot PRs for Action major versions when compatibility is higher than 90% 36 | if: ${{steps.metadata.outputs.package-ecosystem == 'github_actions' && steps.metadata.outputs.update-type == 'version-update:semver-major' && steps.metadata.outputs.compatibility-score >= 90}} 37 | run: gh pr merge --auto --merge "$PR_URL" 38 | env: 39 | PR_URL: ${{github.event.pull_request.html_url}} 40 | GH_TOKEN: ${{secrets.GITHUB_TOKEN}} 41 | -------------------------------------------------------------------------------- /.github/workflows/php-cs-fixer.yml: -------------------------------------------------------------------------------- 1 | name: Fix PHP code style issues 2 | 3 | on: 4 | push: 5 | paths: 6 | - '**.php' 7 | 8 | permissions: 9 | contents: write 10 | 11 | jobs: 12 | php-code-styling: 13 | runs-on: ubuntu-latest 14 | timeout-minutes: 5 15 | 16 | steps: 17 | - name: Checkout code 18 | uses: actions/checkout@v4 19 | with: 20 | ref: ${{ github.head_ref }} 21 | 22 | - name: Fix PHP code style issues 23 | uses: aglipanci/laravel-pint-action@2.5 24 | 25 | - name: Commit changes 26 | uses: stefanzweifel/git-auto-commit-action@v5 27 | with: 28 | commit_message: Fix styling 29 | -------------------------------------------------------------------------------- /.github/workflows/run-tests.yml: -------------------------------------------------------------------------------- 1 | name: run-tests 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | test: 7 | runs-on: ${{ matrix.os }} 8 | strategy: 9 | fail-fast: true 10 | matrix: 11 | os: [ubuntu-latest] 12 | php: [8.4, 8.3, 8.2, 8.1, 8.0] 13 | laravel: [12.*, 11.*, 10.*] 14 | stability: [prefer-lowest, prefer-stable] 15 | include: 16 | - laravel: 12.* 17 | testbench: 10.* 18 | - laravel: 11.* 19 | testbench: 9.* 20 | - laravel: 10.* 21 | testbench: 8.* 22 | exclude: 23 | - laravel: 12.* 24 | php: 8.1 25 | - laravel: 12.* 26 | php: 8.0 27 | - laravel: 11.* 28 | php: 8.1 29 | - laravel: 11.* 30 | php: 8.0 # 31 | - laravel: 10.* 32 | php: 8.0 33 | 34 | name: P${{ matrix.php }} - L${{ matrix.laravel }} - ${{ matrix.stability }} - ${{ matrix.os }} 35 | 36 | steps: 37 | - name: Checkout code 38 | uses: actions/checkout@v4 39 | 40 | - name: Setup PHP 41 | uses: shivammathur/setup-php@v2 42 | with: 43 | php-version: ${{ matrix.php }} 44 | extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, bcmath, soap, intl, gd, exif, iconv, imagick 45 | coverage: none 46 | 47 | - name: Install dependencies 48 | run: | 49 | composer require "laravel/framework:${{ matrix.laravel }}" "orchestra/testbench:${{ matrix.testbench }}" --no-interaction 50 | composer update --${{ matrix.stability }} --prefer-dist --no-interaction 51 | 52 | - name: Execute tests 53 | run: vendor/bin/pest 54 | -------------------------------------------------------------------------------- /.github/workflows/update-changelog.yml: -------------------------------------------------------------------------------- 1 | name: "Update Changelog" 2 | 3 | on: 4 | release: 5 | types: [released] 6 | 7 | jobs: 8 | update: 9 | runs-on: ubuntu-latest 10 | 11 | steps: 12 | - name: Checkout code 13 | uses: actions/checkout@v4 14 | with: 15 | ref: main 16 | 17 | - name: Update Changelog 18 | uses: stefanzweifel/changelog-updater-action@v1 19 | with: 20 | latest-version: ${{ github.event.release.name }} 21 | release-notes: ${{ github.event.release.body }} 22 | 23 | - name: Commit updated CHANGELOG 24 | uses: stefanzweifel/git-auto-commit-action@v5 25 | with: 26 | branch: main 27 | commit_message: Update CHANGELOG 28 | file_pattern: CHANGELOG.md 29 | -------------------------------------------------------------------------------- /.phpunit.cache/test-results: -------------------------------------------------------------------------------- 1 | {"version":"pest_3.7.4","defects":[],"times":{"P\\Tests\\EventTest::__pest_evaluable_it_will_fire_an_event_when_a_translation_has_been_set":0.013,"P\\Tests\\TranslatableTest::__pest_evaluable_it_will_return_package_fallback_locale_translation_when_getting_an_unknown_locale":0.008,"P\\Tests\\TranslatableTest::__pest_evaluable_it_will_return_default_fallback_locale_translation_when_getting_an_unknown_locale":0,"P\\Tests\\TranslatableTest::__pest_evaluable_it_provides_a_flog_to_not_return_fallback_locale_translation_when_getting_an_unknown_locale":0,"P\\Tests\\TranslatableTest::__pest_evaluable_it_will_return_fallback_locale_translation_when_getting_an_unknown_locale_and_fallback_is_true":0,"P\\Tests\\TranslatableTest::__pest_evaluable_it_will_execute_callback_fallback_when_getting_an_unknown_locale_and_fallback_callback_is_enabled":0.005,"P\\Tests\\TranslatableTest::__pest_evaluable_it_will_use_callback_fallback_return_value_as_translation":0,"P\\Tests\\TranslatableTest::__pest_evaluable_it_wont_use_callback_fallback_return_value_as_translation_if_it_is_not_a_string":0,"P\\Tests\\TranslatableTest::__pest_evaluable_it_wont_execute_callback_fallback_when_getting_an_existing_translation":0.001,"P\\Tests\\TranslatableTest::__pest_evaluable_it_wont_fail_if_callback_fallback_throw_exception":0,"P\\Tests\\TranslatableTest::__pest_evaluable_it_will_return_an_empty_string_when_getting_an_unknown_locale_and_fallback_is_not_set":0,"P\\Tests\\TranslatableTest::__pest_evaluable_it_will_return_an_empty_string_when_getting_an_unknown_locale_and_fallback_is_empty":0,"P\\Tests\\TranslatableTest::__pest_evaluable_it_can_save_a_translated_attribute":0,"P\\Tests\\TranslatableTest::__pest_evaluable_it_can_set_translated_values_when_creating_a_model":0,"P\\Tests\\TranslatableTest::__pest_evaluable_it_can_save_multiple_translations":0,"P\\Tests\\TranslatableTest::__pest_evaluable_it_will_return_the_value_of_the_current_locale_when_using_the_property":0.002,"P\\Tests\\TranslatableTest::__pest_evaluable_it_can_get_all_translations_in_one_go":0,"P\\Tests\\TranslatableTest::__pest_evaluable_it_can_get_specified_translations_in_one_go":0,"P\\Tests\\TranslatableTest::__pest_evaluable_it_can_get_all_translations_for_all_translatable_attributes_in_one_go":0,"P\\Tests\\TranslatableTest::__pest_evaluable_it_can_get_specified_translations_for_all_translatable_attributes_in_one_go":0,"P\\Tests\\TranslatableTest::__pest_evaluable_it_can_get_the_locales_which_have_a_translation":0,"P\\Tests\\TranslatableTest::__pest_evaluable_it_can_forget_a_translation":0,"P\\Tests\\TranslatableTest::__pest_evaluable_it_can_forget_all_translations_of_field":0.002,"P\\Tests\\TranslatableTest::__pest_evaluable_it_can_forget_all_translations_of_field_and_make_field_null":0.001,"P\\Tests\\TranslatableTest::__pest_evaluable_it_can_forget_a_field_with_mutator_translation":0,"P\\Tests\\TranslatableTest::__pest_evaluable_it_can_forget_all_translations":0.001,"P\\Tests\\TranslatableTest::__pest_evaluable_it_will_throw_an_exception_when_trying_to_translate_an_untranslatable_attribute":0.001,"P\\Tests\\TranslatableTest::__pest_evaluable_it_is_compatible_with_accessors_on_non_translatable_attributes":0.001,"P\\Tests\\TranslatableTest::__pest_evaluable_it_can_use_accessors_on_translated_attributes":0,"P\\Tests\\TranslatableTest::__pest_evaluable_it_can_be_converted_to_array_when_using_accessors_on_translated_attributes":0.002,"P\\Tests\\TranslatableTest::__pest_evaluable_it_can_use_mutators_on_translated_attributes":0,"P\\Tests\\TranslatableTest::__pest_evaluable_it_can_set_translations_for_default_language":0.001,"P\\Tests\\TranslatableTest::__pest_evaluable_it_can_set_multiple_translations_at_once":0,"P\\Tests\\TranslatableTest::__pest_evaluable_it_can_check_if_an_attribute_is_translatable":0,"P\\Tests\\TranslatableTest::__pest_evaluable_it_can_check_if_an_attribute_has_translation":0,"P\\Tests\\TranslatableTest::__pest_evaluable_it_can_correctly_set_a_field_when_a_mutator_is_defined":0,"P\\Tests\\TranslatableTest::__pest_evaluable_it_can_set_multiple_translations_when_a_mutator_is_defined":0.001,"P\\Tests\\TranslatableTest::__pest_evaluable_it_can_set_multiple_translations_on_field_when_a_mutator_is_defined":0,"P\\Tests\\TranslatableTest::__pest_evaluable_it_uses_the_attribute_to_mutate_the_translated_value":0.001,"P\\Tests\\TranslatableTest::__pest_evaluable_it_can_translate_a_field_based_on_the_translations_of_another_one":0.001,"P\\Tests\\TranslatableTest::__pest_evaluable_it_handle_null_value_from_database":0.001,"P\\Tests\\TranslatableTest::__pest_evaluable_it_can_get_all_translations":0.002,"P\\Tests\\TranslatableTest::__pest_evaluable_it_will_return_fallback_locale_translation_when_getting_an_empty_translation_from_the_locale":0.001,"P\\Tests\\TranslatableTest::__pest_evaluable_it_will_return_correct_translation_value_if_value_is_set_to_zero":0.001,"P\\Tests\\TranslatableTest::__pest_evaluable_it_will_not_return_fallback_value_if_value_is_set_to_zero":0.001,"P\\Tests\\TranslatableTest::__pest_evaluable_it_will_not_remove_zero_value_of_other_locale_in_database":0,"P\\Tests\\TranslatableTest::__pest_evaluable_it_can_be_translated_based_on_given_locale":0,"P\\Tests\\TranslatableTest::__pest_evaluable_it_can_set_and_fetch_attributes_based_on_set_locale":0,"P\\Tests\\TranslatableTest::__pest_evaluable_it_can_replace_translations":0.001,"P\\Tests\\TranslatableTest::__pest_evaluable_it_can_use_any_locale_if_given_locale_not_set":0.001,"P\\Tests\\TranslatableTest::__pest_evaluable_it_will_return_set_translation_when_fallback_any_set":0.002,"P\\Tests\\TranslatableTest::__pest_evaluable_it_will_return_fallback_translation_when_fallback_any_set":0.001,"P\\Tests\\TranslatableTest::__pest_evaluable_it_provides_a_flog_to_not_return_any_translation_when_getting_an_unknown_locale":0.001,"P\\Tests\\TranslatableTest::__pest_evaluable_it_will_return_default_fallback_locale_translation_when_getting_an_unknown_locale_with_fallback_any":0.001,"P\\Tests\\TranslatableTest::__pest_evaluable_it_will_return_all_locales_when_getting_all_translations":0.001,"P\\Tests\\TranslatableTest::__pest_evaluable_it_queries_the_database_whether_a_locale_exists":0.002,"P\\Tests\\TranslatableTest::__pest_evaluable_it_queries_the_database_for_multiple_locales":0.001,"P\\Tests\\TranslatableTest::__pest_evaluable_it_queries_the_database_whether_a_value_exists_in_a_locale":0.001,"P\\Tests\\TranslatableTest::__pest_evaluable_it_queries_the_database_whether_a_value_exists_in_a_multiple_locales":0.001,"P\\Tests\\TranslatableTest::__pest_evaluable_it_can_disable_attribute_locale_fallback_on_a_per_model_basis":0.001,"P\\Tests\\TranslatableTest::__pest_evaluable_it_can_set_fallback_locale_on_model":0.001,"P\\Tests\\TranslatableTest::__pest_evaluable_it_translations_macro_meets_expectations#(['english'], 'en', 'english')":0,"P\\Tests\\TranslatableTest::__pest_evaluable_it_translations_macro_meets_expectations#(['english', 'english'], ['en', 'nl'], 'english')":0,"P\\Tests\\TranslatableTest::__pest_evaluable_it_translations_macro_meets_expectations#(['english', 'dutch'], ['en', 'nl'], ['english', 'dutch'])":0,"P\\Tests\\TranslatableTest::__pest_evaluable_it_should_return_null_when_the_underlying_attribute_in_database_is_null":0,"P\\Tests\\TranslatableTest::__pest_evaluable_it_should_return_locales_with_empty_string_translations_when_allowEmptyStringForTranslation_is_true":0.001,"P\\Tests\\TranslatableTest::__pest_evaluable_it_should_not_return_locales_with_empty_string_translations_when_allowEmptyStringForTranslation_is_false":0,"P\\Tests\\TranslatableTest::__pest_evaluable_it_should_return_locales_with_null_translations_when_allowNullForTranslation_is_true":0,"P\\Tests\\TranslatableTest::__pest_evaluable_it_should_not_return_locales_with_null_translations_when_allowNullForTranslation_is_false":0,"P\\Tests\\TranslatableTest::__pest_evaluable_it_can_set_an_array_list_as_value_for_translation_using__setTranslation_":0.001,"P\\Tests\\TranslatableTest::__pest_evaluable_it_can_set_an_array_list_as_value_for_translation_using_default_local":0.001,"P\\Tests\\TranslatableTest::__pest_evaluable_it_can_treat_an_empty_array_as_value_for_clearing_translations":0.001,"P\\Tests\\TranslatableTest::__pest_evaluable_it_can_set_and_retrieve_translations_for_nested_fields":0.001,"P\\Tests\\TranslatableTest::__pest_evaluable_it_uses_mutators_for_setting_and_getting_translated_values_of_nested_fields":0.001,"P\\Tests\\TranslatableTest::__pest_evaluable_it_should_return_null_when_translation_is_null_and_allowNullForTranslation_is_true":0,"P\\Tests\\TranslatableTest::__pest_evaluable_it_should_return_empty_string_when_translation_is_null_and_allowNullForTranslation_is_false":0}} -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to `laravel-translatable` will be documented in this file 4 | 5 | ## 6.11.4 - 2025-02-20 6 | 7 | **Full Changelog**: https://github.com/spatie/laravel-translatable/compare/6.11.3...6.11.4 8 | 9 | ## 6.11.3 - 2025-02-14 10 | 11 | ### What's Changed 12 | 13 | * Allow null value in translations if allowNullForTranslation is true by @dont-know-php in https://github.com/spatie/laravel-translatable/pull/488 14 | 15 | ### New Contributors 16 | 17 | * @dont-know-php made their first contribution in https://github.com/spatie/laravel-translatable/pull/488 18 | 19 | **Full Changelog**: https://github.com/spatie/laravel-translatable/compare/6.10.2...6.11.3 20 | 21 | ## 6.10.2 - 2025-02-03 22 | 23 | ### What's Changed 24 | 25 | * Fix casts on initialization of HasTranslation by @thaqebon in https://github.com/spatie/laravel-translatable/pull/486 26 | 27 | **Full Changelog**: https://github.com/spatie/laravel-translatable/compare/6.10.1...6.10.2 28 | 29 | ## 6.10.1 - 2025-01-31 30 | 31 | ### What's Changed 32 | 33 | * Handle null database values as null in translations by @alipadron in https://github.com/spatie/laravel-translatable/pull/479 34 | 35 | **Full Changelog**: https://github.com/spatie/laravel-translatable/compare/6.10.0...6.10.1 36 | 37 | ## 6.10.0 - 2025-01-31 38 | 39 | ### What's Changed 40 | 41 | * Support clearing translations using an empty array by @alipadron in https://github.com/spatie/laravel-translatable/pull/478 42 | * Bump dependabot/fetch-metadata from 2.2.0 to 2.3.0 by @dependabot in https://github.com/spatie/laravel-translatable/pull/484 43 | * Add support for nested key translations by @thaqebon in https://github.com/spatie/laravel-translatable/pull/483 44 | 45 | ### New Contributors 46 | 47 | * @thaqebon made their first contribution in https://github.com/spatie/laravel-translatable/pull/483 48 | 49 | **Full Changelog**: https://github.com/spatie/laravel-translatable/compare/6.9.3...6.10.0 50 | 51 | ## 6.9.3 - 2024-12-16 52 | 53 | ### What's Changed 54 | 55 | * Revert return value change when column value is `null` by @vencelkatai in https://github.com/spatie/laravel-translatable/pull/474 56 | 57 | **Full Changelog**: https://github.com/spatie/laravel-translatable/compare/6.9.2...6.9.3 58 | 59 | ## 6.9.2 - 2024-12-11 60 | 61 | ### What's Changed 62 | 63 | * Improve `setAttribute` to handle array list as value for translation by @alipadron in https://github.com/spatie/laravel-translatable/pull/469 64 | 65 | **Full Changelog**: https://github.com/spatie/laravel-translatable/compare/6.9.1...6.9.2 66 | 67 | ## 6.9.1 - 2024-12-11 68 | 69 | ### What's Changed 70 | 71 | * Fix attribute mutators by @vencelkatai in https://github.com/spatie/laravel-translatable/pull/470 72 | 73 | **Full Changelog**: https://github.com/spatie/laravel-translatable/compare/6.9.0...6.9.1 74 | 75 | ## 6.9.0 - 2024-12-09 76 | 77 | ### What's Changed 78 | 79 | * PHP 8.4 deprecates implicitly nullable parameter types. by @selfsimilar in https://github.com/spatie/laravel-translatable/pull/458 80 | * Add .idea to .gitignore, PHP CS Fixer to dev dependencies, and rename PHP CS Fixer config by @alipadron in https://github.com/spatie/laravel-translatable/pull/466 81 | * Allow configuration for handling null and empty strings in translations (Fixes #456) by @alipadron in https://github.com/spatie/laravel-translatable/pull/465 82 | 83 | ### New Contributors 84 | 85 | * @selfsimilar made their first contribution in https://github.com/spatie/laravel-translatable/pull/458 86 | * @alipadron made their first contribution in https://github.com/spatie/laravel-translatable/pull/466 87 | 88 | **Full Changelog**: https://github.com/spatie/laravel-translatable/compare/6.8.0...6.9.0 89 | 90 | ## 6.8.0 - 2024-07-24 91 | 92 | ### What's Changed 93 | 94 | * Bump dependabot/fetch-metadata from 2.1.0 to 2.2.0 by @dependabot in https://github.com/spatie/laravel-translatable/pull/453 95 | * Added operand for json scopes by @rcerljenko in https://github.com/spatie/laravel-translatable/pull/454 96 | 97 | ### New Contributors 98 | 99 | * @rcerljenko made their first contribution in https://github.com/spatie/laravel-translatable/pull/454 100 | 101 | **Full Changelog**: https://github.com/spatie/laravel-translatable/compare/6.7.1...6.8.0 102 | 103 | ## 6.7.1 - 2024-05-14 104 | 105 | ### What's Changed 106 | 107 | * fix: PHPDoc block in Translatable facade by @kyryl-bogach in https://github.com/spatie/laravel-translatable/pull/448 108 | * Bump dependabot/fetch-metadata from 1.6.0 to 2.1.0 by @dependabot in https://github.com/spatie/laravel-translatable/pull/446 109 | 110 | ### New Contributors 111 | 112 | * @kyryl-bogach made their first contribution in https://github.com/spatie/laravel-translatable/pull/448 113 | 114 | **Full Changelog**: https://github.com/spatie/laravel-translatable/compare/6.7.0...6.7.1 115 | 116 | ## 6.7.0 - 2024-05-13 117 | 118 | ### What's Changed 119 | 120 | * Add method comment to Facade for IDE autocompletion by @Muetze42 in https://github.com/spatie/laravel-translatable/pull/438 121 | * Docs: add type declarations `array $translatable` by @fahrim in https://github.com/spatie/laravel-translatable/pull/441 122 | * [FEAT] add ability for filtering a column's locale or multiple locale… by @AbdelrahmanBl in https://github.com/spatie/laravel-translatable/pull/447 123 | 124 | ### New Contributors 125 | 126 | * @fahrim made their first contribution in https://github.com/spatie/laravel-translatable/pull/441 127 | * @AbdelrahmanBl made their first contribution in https://github.com/spatie/laravel-translatable/pull/447 128 | 129 | **Full Changelog**: https://github.com/spatie/laravel-translatable/compare/6.6.2...6.7.0 130 | 131 | ## 6.6.2 - 2024-03-01 132 | 133 | ### What's Changed 134 | 135 | * Fix toArray when using accessors on translatable attributes by @vencelkatai in https://github.com/spatie/laravel-translatable/pull/437 136 | 137 | ### New Contributors 138 | 139 | * @vencelkatai made their first contribution in https://github.com/spatie/laravel-translatable/pull/437 140 | 141 | **Full Changelog**: https://github.com/spatie/laravel-translatable/compare/6.6.1...6.6.2 142 | 143 | ## 6.6.1 - 2024-02-26 144 | 145 | ### What's Changed 146 | 147 | * fix: allow raw searchable umlauts by @Muetze42 in https://github.com/spatie/laravel-translatable/pull/436 148 | 149 | ### New Contributors 150 | 151 | * @Muetze42 made their first contribution in https://github.com/spatie/laravel-translatable/pull/436 152 | 153 | **Full Changelog**: https://github.com/spatie/laravel-translatable/compare/6.6.0...6.6.1 154 | 155 | ## 6.6.0 - 2024-02-23 156 | 157 | ### What's Changed 158 | 159 | * Add laravel 11 support by @mokhosh in https://github.com/spatie/laravel-translatable/pull/434 160 | 161 | ### New Contributors 162 | 163 | * @mokhosh made their first contribution in https://github.com/spatie/laravel-translatable/pull/434 164 | 165 | **Full Changelog**: https://github.com/spatie/laravel-translatable/compare/6.5.5...6.6.0 166 | 167 | ## 6.5.5 - 2023-12-06 168 | 169 | ### What's Changed 170 | 171 | * Revert "Keep null value" by @mabdullahsari in https://github.com/spatie/laravel-translatable/pull/428 172 | 173 | ### New Contributors 174 | 175 | * @mabdullahsari made their first contribution in https://github.com/spatie/laravel-translatable/pull/428 176 | 177 | **Full Changelog**: https://github.com/spatie/laravel-translatable/compare/6.5.4...6.5.5 178 | 179 | ## 6.5.4 - 2023-12-01 180 | 181 | ### What's Changed 182 | 183 | * Bump actions/checkout from 3 to 4 by @dependabot in https://github.com/spatie/laravel-translatable/pull/413 184 | * Keep the number of translations even with null values by @sdebacker in https://github.com/spatie/laravel-translatable/pull/427 185 | * Bump stefanzweifel/git-auto-commit-action from 4 to 5 by @dependabot in https://github.com/spatie/laravel-translatable/pull/418 186 | 187 | ### New Contributors 188 | 189 | * @sdebacker made their first contribution in https://github.com/spatie/laravel-translatable/pull/427 190 | 191 | **Full Changelog**: https://github.com/spatie/laravel-translatable/compare/6.5.3...6.5.4 192 | 193 | ## 6.5.3 - 2023-07-19 194 | 195 | ### What's Changed 196 | 197 | - Bump dependabot/fetch-metadata from 1.5.1 to 1.6.0 by @dependabot in https://github.com/spatie/laravel-translatable/pull/398 198 | - handle new attribute mutator :boom: by @messi89 in https://github.com/spatie/laravel-translatable/pull/402 199 | 200 | ### New Contributors 201 | 202 | - @messi89 made their first contribution in https://github.com/spatie/laravel-translatable/pull/402 203 | 204 | **Full Changelog**: https://github.com/spatie/laravel-translatable/compare/6.5.2...6.5.3 205 | 206 | ## 6.5.2 - 2023-06-20 207 | 208 | ### What's Changed 209 | 210 | - Bump dependabot/fetch-metadata from 1.4.0 to 1.5.1 by @dependabot in https://github.com/spatie/laravel-translatable/pull/394 211 | - Convert static methods to scopes by @gdebrauwer in https://github.com/spatie/laravel-translatable/pull/396 212 | 213 | **Full Changelog**: https://github.com/spatie/laravel-translatable/compare/6.5.1...6.5.2 214 | 215 | ## 6.5.1 - 2023-05-06 216 | 217 | ### What's Changed 218 | 219 | - Bump dependabot/fetch-metadata from 1.3.6 to 1.4.0 by @dependabot in https://github.com/spatie/laravel-translatable/pull/389 220 | - Add getFallbackLocale method by @gdebrauwer in https://github.com/spatie/laravel-translatable/pull/391 221 | 222 | **Full Changelog**: https://github.com/spatie/laravel-translatable/compare/6.5.0...6.5.1 223 | 224 | ## 6.5.0 - 2023-04-20 225 | 226 | ### What's Changed 227 | 228 | - update customize-the-toarray-method.md by @moham96 in https://github.com/spatie/laravel-translatable/pull/387 229 | - Add macro for `$this->translations()` in factories by @bram-pkg in https://github.com/spatie/laravel-translatable/pull/382 230 | 231 | ### New Contributors 232 | 233 | - @moham96 made their first contribution in https://github.com/spatie/laravel-translatable/pull/387 234 | - @bram-pkg made their first contribution in https://github.com/spatie/laravel-translatable/pull/382 235 | 236 | **Full Changelog**: https://github.com/spatie/laravel-translatable/compare/6.4.0...6.5.0 237 | 238 | ## 6.4.0 - 2023-03-19 239 | 240 | ### What's Changed 241 | 242 | - Bump dependabot/fetch-metadata from 1.3.5 to 1.3.6 by @dependabot in https://github.com/spatie/laravel-translatable/pull/376 243 | - Fix badge with `tests` status in `README.md` by @gomzyakov in https://github.com/spatie/laravel-translatable/pull/377 244 | - Update README.md by @alirezasalehizadeh in https://github.com/spatie/laravel-translatable/pull/381 245 | - Enable fallback locale on a per model basis by @yoeriboven in https://github.com/spatie/laravel-translatable/pull/380 246 | 247 | ### New Contributors 248 | 249 | - @gomzyakov made their first contribution in https://github.com/spatie/laravel-translatable/pull/377 250 | - @alirezasalehizadeh made their first contribution in https://github.com/spatie/laravel-translatable/pull/381 251 | - @yoeriboven made their first contribution in https://github.com/spatie/laravel-translatable/pull/380 252 | 253 | **Full Changelog**: https://github.com/spatie/laravel-translatable/compare/6.3.0...6.4.0 254 | 255 | ## 6.3.0 - 2023-01-14 256 | 257 | ### What's Changed 258 | 259 | - Laravel 10.x support by @erikn69 in https://github.com/spatie/laravel-translatable/pull/374 260 | 261 | ### New Contributors 262 | 263 | - @erikn69 made their first contribution in https://github.com/spatie/laravel-translatable/pull/374 264 | 265 | **Full Changelog**: https://github.com/spatie/laravel-translatable/compare/6.2.0...6.3.0 266 | 267 | ## 6.2.0 - 2022-12-23 268 | 269 | ### What's Changed 270 | 271 | - Add Dependabot Automation by @patinthehat in https://github.com/spatie/laravel-translatable/pull/366 272 | - Add PHP 8.2 Support by @patinthehat in https://github.com/spatie/laravel-translatable/pull/367 273 | - Bump actions/checkout from 2 to 3 by @dependabot in https://github.com/spatie/laravel-translatable/pull/368 274 | - Added whereLocale and whereLocales methods by @ahmetbarut in https://github.com/spatie/laravel-translatable/pull/370 275 | 276 | ### New Contributors 277 | 278 | - @dependabot made their first contribution in https://github.com/spatie/laravel-translatable/pull/368 279 | 280 | **Full Changelog**: https://github.com/spatie/laravel-translatable/compare/6.1.0...6.2.0 281 | 282 | ## 6.1.0 - 2022-10-21 283 | 284 | ### What's Changed 285 | 286 | - PHPUnit to Pest Converter by @freekmurze in https://github.com/spatie/laravel-translatable/pull/335 287 | - Fix typo in "Getting and setting translations" by @sami-cha in https://github.com/spatie/laravel-translatable/pull/346 288 | - Fix typo in advanced usage docs directory name by @greatislander in https://github.com/spatie/laravel-translatable/pull/347 289 | - Fixed example for forgetAllTranslations() method. by @odeland in https://github.com/spatie/laravel-translatable/pull/348 290 | - added locales method by @ahmetbarut in https://github.com/spatie/laravel-translatable/pull/361 291 | 292 | ### New Contributors 293 | 294 | - @sami-cha made their first contribution in https://github.com/spatie/laravel-translatable/pull/346 295 | - @greatislander made their first contribution in https://github.com/spatie/laravel-translatable/pull/347 296 | - @odeland made their first contribution in https://github.com/spatie/laravel-translatable/pull/348 297 | - @ahmetbarut made their first contribution in https://github.com/spatie/laravel-translatable/pull/361 298 | 299 | **Full Changelog**: https://github.com/spatie/laravel-translatable/compare/6.0.0...6.1.0 300 | 301 | ## 6.0.0 - 2022-03-07 302 | 303 | - improved fallback customisations 304 | - modernized code base 305 | - drop support for Laravel 8 306 | 307 | ## 5.2.0 - 2022-01-13 308 | 309 | - support Laravel 9 310 | 311 | ## 5.0.3 - 2021-10-04 312 | 313 | - solve the string value issue in filterTranslations method (#300) 314 | 315 | ## 5.0.2 - 2021-09-28 316 | 317 | - specify locales in get translations method (#299) 318 | 319 | ## 5.0.1 - 2021-07-15 320 | 321 | - fix return types of getTranslation (#286) 322 | 323 | ## 5.0.0 - 2021-03-26 324 | 325 | - require PHP 8+ 326 | - convert syntax to PHP 8 327 | - drop support for PHP 7.x 328 | - drop support for Laravel 6.x 329 | - implement `spatie/laravel-package-tools` 330 | 331 | ## 4.6.0 - 2020-11-19 332 | 333 | - add support for PHP 8.0 (#241) 334 | - drop support for Laravel 5.8 (#241) 335 | 336 | ## 4.5.2 - 2020-10-22 337 | 338 | - revert #235 339 | 340 | ## 4.5.1 - 2020-10-22 341 | 342 | - use string casting for translatable columns (#235) 343 | 344 | ## 4.5.0 2020-10-03 345 | 346 | - add replaceTranslations method (#231) 347 | 348 | ## 4.4.3 - 2020-10-2 349 | 350 | - rename `withLocale` to `usingLocale` 351 | 352 | ## 4.4.2 - 2020-10-02 353 | 354 | - elegant syntax update (#229) 355 | 356 | ## 4.4.1 - 2020-09-06 357 | 358 | - add support for Laravel 8 (#226) 359 | 360 | ## 4.4.0 - 2020-07-09 361 | 362 | - make possible to set multiple translations on mutator model field with array (#216) 363 | 364 | ## 4.3.2 - 2020-04-30 365 | 366 | - fix `forgetTranslation` & `forgetAllTranslations` on fields with mutator (#205) 367 | 368 | ## 4.3.1 - 2020-03-07 369 | 370 | - Lumen fix (#201) 371 | 372 | ## 4.3.0 - 2020-03-02 373 | 374 | - add support for Laravel 7 375 | 376 | ## 4.2.2 - 2020-01-20 377 | 378 | - open up for non-model objects (#186) 379 | 380 | ## 4.2.1 - 2019-10-03 381 | 382 | - add third param to translate method (#177) 383 | 384 | ## 4.2.0 - 2019-09-04 385 | 386 | - make compatible with Laravel 6 387 | 388 | ## 4.1.4 - 2019-08-28 389 | 390 | - re-added the `translatable.fallback_local` config which overrule `app.fallback_local` (see https://github.com/spatie/laravel-translatable/issues/170) 391 | 392 | ## 4.1.3 - 2019-06-16 393 | 394 | - improve dependencies 395 | 396 | ## 4.1.2 - 2019-06-06 397 | 398 | - allow false and true values in translations 399 | 400 | ## 4.1.1 - 2019-02-27 401 | 402 | - fix service provider error 403 | 404 | ## 4.1.0 - 2019-02-27 405 | 406 | - drop support for Laravel 5.7 and below 407 | - drop support for PHP 7.1 and below 408 | 409 | ## 4.0.0 - 2019-02-27 410 | 411 | - `app.fallback_local` will now be used (see #148) 412 | 413 | ## 3.1.3 - 2019-02-27 414 | 415 | - add support for Laravel 5.8 416 | 417 | ## 3.1.2 - 2019-01-05 418 | 419 | - add `hasTranslation` 420 | 421 | ## 3.1.1 - 2018-12-18 422 | 423 | - allow 0 to be used as a translation value 424 | 425 | ## 3.1.0 - 2018-11-29 426 | 427 | - allow `getTranslations` to return other things than strings 428 | 429 | ## 3.0.1 - 2018-09-18 430 | 431 | - fix regarding empty locales 432 | 433 | ## 3.0.0 - 2018-09-16 434 | 435 | - added `translations` accessor 436 | - dropped support for PHP 7.0 437 | 438 | ## 2.2.1 - 2018-08-24 439 | 440 | - add support for Laravel 5.7 441 | 442 | ## 2.2.0 - 2018-03-09 443 | 444 | - made it possible to get all translations in one go 445 | 446 | ## 2.1.5 - 2018-02-28 447 | 448 | - better handling of `null` values 449 | 450 | ## 2.1.4 - 2018-02-08 451 | 452 | - add support for L5.6 453 | 454 | ## 2.1.3 - 2018-01-24 455 | 456 | - make locale handling more flexible 457 | 458 | ## 2.1.2 - 2017-12-24 459 | 460 | - fix for using translations within translations 461 | 462 | ## 2.1.1 - 2017-12-20 463 | 464 | - fix event `key` attribute 465 | - fix support for mutators 466 | 467 | ## 2.1.0 - 2017-09-21 468 | 469 | - added support for setting a translation directly through the property 470 | 471 | ## 2.0.0 - 2017-08-30 472 | 473 | - added support for Laravel 5.5, dropped support for all older versions 474 | - rename config file from `laravel-translatable` to `translatable` 475 | 476 | ## 1.3.0 - 2017-06-12 477 | 478 | - add `forgetAllTranslations` 479 | 480 | ## 1.2.2 - 2016-01-27 481 | 482 | - improve support for fallback locale 483 | 484 | ## 1.2.1 - 2016-01-23 485 | 486 | - improve compatibility for Laravel 5.4 487 | 488 | ## 1.2.0 - 2016-01-23 489 | 490 | - add compatibility for Laravel 5.4 491 | 492 | ## 1.1.2 - 2016-10-02 493 | 494 | - made `isTranslatableAttribute` public 495 | 496 | ## 1.1.1 - 2016-08-24 497 | 498 | - add L5.3 compatibility 499 | 500 | ## 1.1.0 - 2016-05-02 501 | 502 | - added support for a fallback locale 503 | 504 | ## 1.0.0 - 2016-04-10 505 | 506 | - initial release 507 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) Spatie bvba 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | 5 | Logo for laravel-translatable 6 | 7 | 8 | 9 |

A trait to make Eloquent models translatable

10 | 11 | [![Latest Version on Packagist](https://img.shields.io/packagist/v/spatie/laravel-translatable.svg?style=flat-square)](https://packagist.org/packages/spatie/laravel-translatable) 12 | [![MIT Licensed](https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square)](LICENSE.md) 13 | ![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/spatie/laravel-translatable/run-tests.yml) 14 | [![Total Downloads](https://img.shields.io/packagist/dt/spatie/laravel-translatable.svg?style=flat-square)](https://packagist.org/packages/spatie/laravel-translatable) 15 | 16 |
17 | 18 | This package contains a trait `HasTranslations` to make Eloquent models translatable. Translations are stored as json. There is no extra table needed to hold them. 19 | 20 | ```php 21 | use Illuminate\Database\Eloquent\Model; 22 | use Spatie\Translatable\HasTranslations; 23 | 24 | class NewsItem extends Model 25 | { 26 | use HasTranslations; 27 | 28 | public $translatable = ['name']; // translatable attributes 29 | 30 | // ... 31 | } 32 | ``` 33 | 34 | After the trait is applied on the model you can do these things: 35 | 36 | ```php 37 | $newsItem = new NewsItem; 38 | $newsItem 39 | ->setTranslation('name', 'en', 'Name in English') 40 | ->setTranslation('name', 'nl', 'Naam in het Nederlands') 41 | ->save(); 42 | 43 | $newsItem->name; // Returns 'Name in English' given that the current app locale is 'en' 44 | $newsItem->getTranslation('name', 'nl'); // returns 'Naam in het Nederlands' 45 | 46 | app()->setLocale('nl'); 47 | $newsItem->name; // Returns 'Naam in het Nederlands' 48 | 49 | $newsItem->getTranslations('name'); // returns an array of all name translations 50 | 51 | // You can translate nested keys of a JSON column using the -> notation 52 | // First, add the path to the $translatable array, e.g., 'meta->description' 53 | $newsItem 54 | ->setTranslation('meta->description', 'en', 'Description in English') 55 | ->setTranslation('meta->description', 'nl', 'Beschrijving in het Nederlands') 56 | ->save(); 57 | 58 | $attributeKey = 'meta->description'; 59 | $newsItem->$attributeKey; // Returns 'Description in English' 60 | $newsItem->getTranslation('meta->description', 'nl'); // Returns 'Beschrijving in het Nederlands' 61 | ``` 62 | 63 | Also providing scoped queries for retrieving records based on locales 64 | 65 | ```php 66 | // Returns all news items with a name in English 67 | NewsItem::whereLocale('name', 'en')->get(); 68 | 69 | // Returns all news items with a name in English or Dutch 70 | NewsItem::whereLocales('name', ['en', 'nl'])->get(); 71 | 72 | // Returns all news items that has name in English with value `Name in English` 73 | NewsItem::query()->whereJsonContainsLocale('name', 'en', 'Name in English')->get(); 74 | 75 | // Returns all news items that has name in English or Dutch with value `Name in English` 76 | NewsItem::query()->whereJsonContainsLocales('name', ['en', 'nl'], 'Name in English')->get(); 77 | 78 | // The last argument is the "operand" which you can tweak to achieve something like this: 79 | 80 | // Returns all news items that has name in English with value like `Name in...` 81 | NewsItem::query()->whereJsonContainsLocale('name', 'en', 'Name in%', 'like')->get(); 82 | 83 | // Returns all news items that has name in English or Dutch with value like `Name in...` 84 | NewsItem::query()->whereJsonContainsLocales('name', ['en', 'nl'], 'Name in%', 'like')->get(); 85 | ``` 86 | 87 | ## Support us 88 | 89 | [](https://spatie.be/github-ad-click/laravel-translatable) 90 | 91 | We invest a lot of resources into creating [best in class open source packages](https://spatie.be/open-source). You can support us by [buying one of our paid products](https://spatie.be/open-source/support-us). 92 | 93 | We highly appreciate you sending us a postcard from your hometown, mentioning which of our package(s) you are using. You'll find our address on [our contact page](https://spatie.be/about-us). We publish all received postcards on [our virtual postcard wall](https://spatie.be/open-source/postcards). 94 | 95 | ## Documentation 96 | 97 | All documentation is available [on our documentation site](https://spatie.be/docs/laravel-translatable). 98 | 99 | ## Testing 100 | 101 | ```bash 102 | composer test 103 | ``` 104 | 105 | ## Contributing 106 | 107 | Please see [CONTRIBUTING](https://github.com/spatie/.github/blob/main/CONTRIBUTING.md) for details. 108 | 109 | ## Security 110 | 111 | If you've found a bug regarding security please mail [security@spatie.be](mailto:security@spatie.be) instead of using the issue tracker. 112 | 113 | ## Postcardware 114 | 115 | You're free to use this package, but if it makes it to your production environment we highly appreciate you sending us a postcard from your hometown, mentioning which of our package(s) you are using. 116 | 117 | Our address is: Spatie, Kruikstraat 22, 2018 Antwerp, Belgium. 118 | 119 | We publish all received postcards [on our company website](https://spatie.be/en/opensource/postcards). 120 | 121 | ## Credits 122 | 123 | - [Freek Van der Herten](https://github.com/freekmurze) 124 | - [Sebastian De Deyne](https://github.com/sebastiandedeyne) 125 | - [All Contributors](../../contributors) 126 | 127 | We got the idea to store translations as json in a column from [Mohamed Said](https://github.com/themsaid). Parts of the readme of [his multilingual package](https://github.com/themsaid/laravel-multilingual) were used in this readme. 128 | 129 | ## Alternatives 130 | 131 | - [DB-Fields-Translations](https://github.com/Afzaal565/DB-Fields-Translations) 132 | 133 | ## License 134 | 135 | The MIT License (MIT). Please see [License File](LICENSE.md) for more information. 136 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "spatie/laravel-translatable", 3 | "description": "A trait to make an Eloquent model hold translations", 4 | "keywords": [ 5 | "spatie", 6 | "laravel-translatable", 7 | "translate", 8 | "eloquent", 9 | "model", 10 | "i8n", 11 | "multilingual" 12 | ], 13 | "homepage": "https://github.com/spatie/laravel-translatable", 14 | "license": "MIT", 15 | "authors": [ 16 | { 17 | "name": "Freek Van der Herten", 18 | "email": "freek@spatie.be", 19 | "homepage": "https://spatie.be", 20 | "role": "Developer" 21 | }, 22 | { 23 | "name": "Sebastian De Deyne", 24 | "email": "sebastian@spatie.be", 25 | "homepage": "https://spatie.be", 26 | "role": "Developer" 27 | } 28 | ], 29 | "require": { 30 | "php": "^8.0", 31 | "illuminate/database": "^10.0|^11.0|^12.0", 32 | "illuminate/support": "^10.0|^11.0|^12.0", 33 | "spatie/laravel-package-tools": "^1.11" 34 | }, 35 | "require-dev": { 36 | "friendsofphp/php-cs-fixer": "^3.64", 37 | "mockery/mockery": "^1.4", 38 | "orchestra/testbench": "^7.0|^8.0|^9.0|^10.0", 39 | "pestphp/pest": "^1.20|^2.0|^3.0" 40 | }, 41 | "autoload": { 42 | "psr-4": { 43 | "Spatie\\Translatable\\": "src" 44 | } 45 | }, 46 | "autoload-dev": { 47 | "psr-4": { 48 | "Spatie\\Translatable\\Test\\": "tests" 49 | } 50 | }, 51 | "scripts": { 52 | "format": "vendor/bin/php-cs-fixer fix --allow-risky=yes", 53 | "test": "vendor/bin/pest" 54 | }, 55 | "config": { 56 | "allow-plugins": { 57 | "pestphp/pest-plugin": true 58 | }, 59 | "sort-packages": true 60 | }, 61 | "extra": { 62 | "laravel": { 63 | "providers": [ 64 | "Spatie\\Translatable\\TranslatableServiceProvider" 65 | ] 66 | }, 67 | "aliases": { 68 | "Translatable": "Spatie\\Translatable\\Facades\\Translatable" 69 | } 70 | }, 71 | "minimum-stability": "dev", 72 | "prefer-stable": true 73 | } 74 | -------------------------------------------------------------------------------- /src/Events/TranslationHasBeenSetEvent.php: -------------------------------------------------------------------------------- 1 | getTranslatableAttributes()); 12 | 13 | return new static("Cannot translate attribute `{$key}` as it's not one of the translatable attributes: `$translatableAttributes`"); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/Facades/Translatable.php: -------------------------------------------------------------------------------- 1 | mergeCasts( 20 | array_fill_keys($this->getTranslatableAttributes(), 'array'), 21 | ); 22 | } 23 | 24 | public static function usingLocale(string $locale): self 25 | { 26 | return (new self)->setLocale($locale); 27 | } 28 | 29 | public function useFallbackLocale(): bool 30 | { 31 | if (property_exists($this, 'useFallbackLocale')) { 32 | return $this->useFallbackLocale; 33 | } 34 | 35 | return true; 36 | } 37 | 38 | public function getAttributeValue($key): mixed 39 | { 40 | if (! $this->isTranslatableAttribute($key)) { 41 | return parent::getAttributeValue($key); 42 | } 43 | 44 | return $this->getTranslation($key, $this->getLocale(), $this->useFallbackLocale()); 45 | } 46 | 47 | protected function mutateAttributeForArray($key, $value): mixed 48 | { 49 | if (! $this->isTranslatableAttribute($key)) { 50 | return parent::mutateAttributeForArray($key, $value); 51 | } 52 | 53 | $translations = $this->getTranslations($key); 54 | 55 | return array_map(fn ($value) => parent::mutateAttributeForArray($key, $value), $translations); 56 | } 57 | 58 | public function setAttribute($key, $value) 59 | { 60 | if (! $this->isTranslatableAttribute($key)) { 61 | return parent::setAttribute($key, $value); 62 | } 63 | 64 | if (is_array($value) && (! array_is_list($value) || count($value) === 0)) { 65 | return $this->setTranslations($key, $value); 66 | } 67 | 68 | return $this->setTranslation($key, $this->getLocale(), $value); 69 | } 70 | 71 | public function translate(string $key, string $locale = '', bool $useFallbackLocale = true): mixed 72 | { 73 | return $this->getTranslation($key, $locale, $useFallbackLocale); 74 | } 75 | 76 | public function getTranslation(string $key, string $locale, bool $useFallbackLocale = true): mixed 77 | { 78 | $normalizedLocale = $this->normalizeLocale($key, $locale, $useFallbackLocale); 79 | 80 | $isKeyMissingFromLocale = ($locale !== $normalizedLocale); 81 | 82 | $translations = $this->getTranslations($key); 83 | 84 | $baseKey = Str::before($key, '->'); // get base key in case it is JSON nested key 85 | 86 | $translatableConfig = app(Translatable::class); 87 | 88 | if (is_null(self::getAttributeFromArray($baseKey))) { 89 | $translation = null; 90 | } else { 91 | $translation = isset($translations[$normalizedLocale]) ? $translations[$normalizedLocale] : null; 92 | $translation ??= ($translatableConfig->allowNullForTranslation) ? null : ''; 93 | } 94 | 95 | if ($isKeyMissingFromLocale && $translatableConfig->missingKeyCallback) { 96 | try { 97 | $callbackReturnValue = ($translatableConfig->missingKeyCallback)($this, $key, $locale, $translation, $normalizedLocale); 98 | if (is_string($callbackReturnValue)) { 99 | $translation = $callbackReturnValue; 100 | } 101 | } catch (Exception) { 102 | // prevent the fallback to crash 103 | } 104 | } 105 | 106 | $key = str_replace('->', '-', $key); 107 | 108 | if ($this->hasGetMutator($key)) { 109 | return $this->mutateAttribute($key, $translation); 110 | } 111 | 112 | if ($this->hasAttributeMutator($key)) { 113 | return $this->mutateAttributeMarkedAttribute($key, $translation); 114 | } 115 | 116 | return $translation; 117 | } 118 | 119 | public function getTranslationWithFallback(string $key, string $locale): mixed 120 | { 121 | return $this->getTranslation($key, $locale, true); 122 | } 123 | 124 | public function getTranslationWithoutFallback(string $key, string $locale): mixed 125 | { 126 | return $this->getTranslation($key, $locale, false); 127 | } 128 | 129 | public function getTranslations(?string $key = null, ?array $allowedLocales = null): array 130 | { 131 | if ($key !== null) { 132 | $this->guardAgainstNonTranslatableAttribute($key); 133 | $translatableConfig = app(Translatable::class); 134 | 135 | if ($this->isNestedKey($key)) { 136 | [$key, $nestedKey] = explode('.', str_replace('->', '.', $key), 2); 137 | } 138 | 139 | return array_filter( 140 | Arr::get($this->fromJson($this->getAttributeFromArray($key)), $nestedKey ?? null, []), 141 | fn ($value, $locale) => $this->filterTranslations($value, $locale, $allowedLocales, $translatableConfig->allowNullForTranslation, $translatableConfig->allowEmptyStringForTranslation), 142 | ARRAY_FILTER_USE_BOTH, 143 | ); 144 | } 145 | 146 | return array_reduce($this->getTranslatableAttributes(), function ($result, $item) use ($allowedLocales) { 147 | $result[$item] = $this->getTranslations($item, $allowedLocales); 148 | 149 | return $result; 150 | }); 151 | } 152 | 153 | public function setTranslation(string $key, string $locale, $value): self 154 | { 155 | $this->guardAgainstNonTranslatableAttribute($key); 156 | 157 | $translations = $this->getTranslations($key); 158 | 159 | $oldValue = $translations[$locale] ?? ''; 160 | 161 | $mutatorKey = str_replace('->', '-', $key); 162 | 163 | if ($this->hasSetMutator($mutatorKey)) { 164 | $method = 'set'.Str::studly($mutatorKey).'Attribute'; 165 | 166 | $this->{$method}($value, $locale); 167 | 168 | $value = $this->attributes[$key]; 169 | } elseif ($this->hasAttributeSetMutator($mutatorKey)) { // handle new attribute mutator 170 | $this->setAttributeMarkedMutatedAttributeValue($mutatorKey, $value); 171 | 172 | $value = $this->attributes[$mutatorKey]; 173 | } 174 | 175 | $translations[$locale] = $value; 176 | 177 | if ($this->isNestedKey($key)) { 178 | unset($this->attributes[$key], $this->attributes[$mutatorKey]); 179 | 180 | $this->fillJsonAttribute($key, $translations); 181 | } else { 182 | $this->attributes[$key] = json_encode($translations, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); 183 | } 184 | 185 | event(new TranslationHasBeenSetEvent($this, $key, $locale, $oldValue, $value)); 186 | 187 | return $this; 188 | } 189 | 190 | public function setTranslations(string $key, array $translations): self 191 | { 192 | $this->guardAgainstNonTranslatableAttribute($key); 193 | 194 | if (! empty($translations)) { 195 | foreach ($translations as $locale => $translation) { 196 | $this->setTranslation($key, $locale, $translation); 197 | } 198 | } else { 199 | $this->attributes[$key] = $this->asJson([]); 200 | } 201 | 202 | return $this; 203 | } 204 | 205 | public function forgetTranslation(string $key, string $locale): self 206 | { 207 | $translations = $this->getTranslations($key); 208 | 209 | unset( 210 | $translations[$locale], 211 | $this->$key 212 | ); 213 | 214 | $this->setTranslations($key, $translations); 215 | 216 | return $this; 217 | } 218 | 219 | public function forgetTranslations(string $key, bool $asNull = false): self 220 | { 221 | $this->guardAgainstNonTranslatableAttribute($key); 222 | 223 | collect($this->getTranslatedLocales($key))->each(function (string $locale) use ($key) { 224 | $this->forgetTranslation($key, $locale); 225 | }); 226 | 227 | if ($asNull) { 228 | $this->attributes[$key] = null; 229 | } 230 | 231 | return $this; 232 | } 233 | 234 | public function forgetAllTranslations(string $locale): self 235 | { 236 | collect($this->getTranslatableAttributes())->each(function (string $attribute) use ($locale) { 237 | $this->forgetTranslation($attribute, $locale); 238 | }); 239 | 240 | return $this; 241 | } 242 | 243 | public function getTranslatedLocales(string $key): array 244 | { 245 | return array_keys($this->getTranslations($key)); 246 | } 247 | 248 | public function isNestedKey(string $key): bool 249 | { 250 | return str_contains($key, '->'); 251 | } 252 | 253 | public function isTranslatableAttribute(string $key): bool 254 | { 255 | return in_array($key, $this->getTranslatableAttributes()); 256 | } 257 | 258 | public function hasTranslation(string $key, ?string $locale = null): bool 259 | { 260 | $locale = $locale ?: $this->getLocale(); 261 | 262 | return isset($this->getTranslations($key)[$locale]); 263 | } 264 | 265 | public function replaceTranslations(string $key, array $translations): self 266 | { 267 | foreach ($this->getTranslatedLocales($key) as $locale) { 268 | $this->forgetTranslation($key, $locale); 269 | } 270 | 271 | $this->setTranslations($key, $translations); 272 | 273 | return $this; 274 | } 275 | 276 | protected function guardAgainstNonTranslatableAttribute(string $key): void 277 | { 278 | if (! $this->isTranslatableAttribute($key)) { 279 | throw AttributeIsNotTranslatable::make($key, $this); 280 | } 281 | } 282 | 283 | protected function normalizeLocale(string $key, string $locale, bool $useFallbackLocale): string 284 | { 285 | $translatedLocales = $this->getTranslatedLocales($key); 286 | 287 | if (in_array($locale, $translatedLocales)) { 288 | return $locale; 289 | } 290 | 291 | if (! $useFallbackLocale) { 292 | return $locale; 293 | } 294 | 295 | if (method_exists($this, 'getFallbackLocale')) { 296 | $fallbackLocale = $this->getFallbackLocale(); 297 | } 298 | 299 | $fallbackConfig = app(Translatable::class); 300 | 301 | $fallbackLocale ??= $fallbackConfig->fallbackLocale ?? config('app.fallback_locale'); 302 | 303 | if (! is_null($fallbackLocale) && in_array($fallbackLocale, $translatedLocales)) { 304 | return $fallbackLocale; 305 | } 306 | 307 | if (! empty($translatedLocales) && $fallbackConfig->fallbackAny) { 308 | return $translatedLocales[0]; 309 | } 310 | 311 | return $locale; 312 | } 313 | 314 | protected function filterTranslations(mixed $value = null, ?string $locale = null, ?array $allowedLocales = null, bool $allowNull = false, bool $allowEmptyString = false): bool 315 | { 316 | if ($value === null && ! $allowNull) { 317 | return false; 318 | } 319 | 320 | if ($value === '' && ! $allowEmptyString) { 321 | return false; 322 | } 323 | 324 | if ($allowedLocales === null) { 325 | return true; 326 | } 327 | 328 | if (! in_array($locale, $allowedLocales)) { 329 | return false; 330 | } 331 | 332 | return true; 333 | } 334 | 335 | public function setLocale(string $locale): self 336 | { 337 | $this->translationLocale = $locale; 338 | 339 | return $this; 340 | } 341 | 342 | public function getLocale(): string 343 | { 344 | return $this->translationLocale ?: config('app.locale'); 345 | } 346 | 347 | public function getTranslatableAttributes(): array 348 | { 349 | return is_array($this->translatable) 350 | ? $this->translatable 351 | : []; 352 | } 353 | 354 | public function translations(): Attribute 355 | { 356 | return Attribute::get(function () { 357 | return collect($this->getTranslatableAttributes()) 358 | ->mapWithKeys(function (string $key) { 359 | return [$key => $this->getTranslations($key)]; 360 | }) 361 | ->toArray(); 362 | }); 363 | } 364 | 365 | public function locales(): array 366 | { 367 | return array_unique( 368 | array_reduce($this->getTranslatableAttributes(), function ($result, $item) { 369 | return array_merge($result, $this->getTranslatedLocales($item)); 370 | }, []) 371 | ); 372 | } 373 | 374 | public function scopeWhereLocale(Builder $query, string $column, string $locale): void 375 | { 376 | $query->whereNotNull("{$column}->{$locale}"); 377 | } 378 | 379 | public function scopeWhereLocales(Builder $query, string $column, array $locales): void 380 | { 381 | $query->where(function (Builder $query) use ($column, $locales) { 382 | foreach ($locales as $locale) { 383 | $query->orWhereNotNull("{$column}->{$locale}"); 384 | } 385 | }); 386 | } 387 | 388 | public function scopeWhereJsonContainsLocale(Builder $query, string $column, string $locale, mixed $value, string $operand = '='): void 389 | { 390 | $query->where("{$column}->{$locale}", $operand, $value); 391 | } 392 | 393 | public function scopeWhereJsonContainsLocales(Builder $query, string $column, array $locales, mixed $value, string $operand = '='): void 394 | { 395 | $query->where(function (Builder $query) use ($column, $locales, $value, $operand) { 396 | foreach ($locales as $locale) { 397 | $query->orWhere("{$column}->{$locale}", $operand, $value); 398 | } 399 | }); 400 | } 401 | 402 | /** 403 | * @deprecated 404 | */ 405 | public static function whereLocale(string $column, string $locale): Builder 406 | { 407 | return static::query()->whereNotNull("{$column}->{$locale}"); 408 | } 409 | 410 | /** 411 | * @deprecated 412 | */ 413 | public static function whereLocales(string $column, array $locales): Builder 414 | { 415 | return static::query()->where(function (Builder $query) use ($column, $locales) { 416 | foreach ($locales as $locale) { 417 | $query->orWhereNotNull("{$column}->{$locale}"); 418 | } 419 | }); 420 | } 421 | } 422 | -------------------------------------------------------------------------------- /src/Translatable.php: -------------------------------------------------------------------------------- 1 | fallbackLocale = $fallbackLocale; 32 | $this->fallbackAny = $fallbackAny; 33 | $this->missingKeyCallback = $missingKeyCallback; 34 | 35 | return $this; 36 | } 37 | 38 | public function allowNullForTranslation(bool $allowNullForTranslation = true): self 39 | { 40 | $this->allowNullForTranslation = $allowNullForTranslation; 41 | 42 | return $this; 43 | } 44 | 45 | public function allowEmptyStringForTranslation(bool $allowEmptyStringForTranslation = true): self 46 | { 47 | $this->allowEmptyStringForTranslation = $allowEmptyStringForTranslation; 48 | 49 | return $this; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/TranslatableServiceProvider.php: -------------------------------------------------------------------------------- 1 | name('laravel-translatable'); 15 | } 16 | 17 | public function packageRegistered(): void 18 | { 19 | $this->app->singleton(Translatable::class, fn () => new Translatable); 20 | $this->app->bind('translatable', Translatable::class); 21 | 22 | Factory::macro('translations', function (string|array $locales, mixed $value) { 23 | return is_array($value) 24 | ? array_combine((array) $locales, $value) 25 | : array_fill_keys((array) $locales, $value); 26 | }); 27 | } 28 | } 29 | --------------------------------------------------------------------------------