alexandre-fernandez / json-translation-bundle
Manage localized content in your Symfony applications.
Requires
- alexandre-fernandez/key-value-form-bundle: ^1.1
- doctrine/doctrine-bundle: ^2.13
- doctrine/orm: ^3.3
- symfony/form: ^7.2
- symfony/framework-bundle: ^7.2
- symfony/translation: ^7.2
- symfony/twig-bundle: ^7.2
README
json-translation-bundle
Manage localized content in your Symfony applications.
Installation
composer require alexandre-fernandez/json-translation-bundle
Verify that the bundle is enabled in config/bundles.php
:
return [
// ...
AlexandreFernandez\JsonTranslationBundle\JsonTranslationBundle::class => ['all' => true],
];
Configuration
# config/packages/json_translation.yaml
json_translation:
# The default locale for the application, used as a fallback when no fallbacks are defined.
# Use "%kernel.default_locale%" to use your application's default locale.
default_locale: "en"
# The enabled locales for the application, these will be the only supported ones, an empty array will accept any locale.
# Use "%kernel.enabled_locales%" to use your application's enabled locales.
enabled_locales: ["en", "fr", "es"]
# Which locale will the application fallback on when the current locale is not found, an empty array will use this configuration's default locale instead.
fallbacks: ["en", "es"]
# If "true", `JsonTranslation` instances will default to the current request's locale, else they will default to this configuration's default locale.
detect_request_locale: true
Basic usage
Add the translated property to its entity.
// src/Entity/Post.php
use App\Repository\PostRepository;
use Doctrine\ORM\Mapping as ORM;
use AlexandreFernandez\JsonTranslationBundle\Model\JsonTranslation;
#[ORM\Entity(repositoryClass: PostRepository::class)]
class Post
{
#[ORM\Column(
type: "json_translation", // same as JsonTranslationType::TYPE (AlexandreFernandez\JsonTranslationBundle\Doctrine\Type\JsonTranslationType)
options: ["jsonb" => true] // to store as jsonb in PostgreSQL
)]
private JsonTranslation $title;
public function __construct()
{
$this->title = new JsonTranslation();
}
public function getTitle(): JsonTranslation
{
return $this->title;
}
public function setTitle(JsonTranslation $title): static
{
$this->title = $title;
return $this;
}
}
You can then populate title
's translations using the setTranslation
method, using ArrayAccess
or using a translation array.
// using setTranslation
$post
->getTitle()
->setTranslation("en", "Hello")
->setTranslation("fr", "Bonjour")
->setTranslation("es", "Hola")
;
// using ArrayAccess
$post->getTitle()["en"] = "Hello";
$post->getTitle()["fr"] = "Bonjour";
$post->getTitle()["es"] = "Hola";
// using an array
$post->setTitle(JsonTranslation::createFromArray([
"en" => "Hello",
"fr" => "Bonjour",
"es" => "Hola"
]));
JsonTranslation
is Traversable
, it can be used in a foreach
.
foreach($title as $locale => $translation) {
// ...
}
JsonTranslation
is Stringable
, it will automatically resolve to the current request locale (this behaviour can be disabled in the configuration).
<h1>{{ post.title }}</h1>
JsonTranslation
Reference
static createFromArray(array $translations): static
Creates a new JsonTranslation
instance from an array of translations (e.g, [ "en" => "hi", "fr" => "salut", "es" => null ]
).
getEntries(): array
Returns all translation entries excluding null values (e.g., [ "en" => "hi", "fr" => "salut" ]
).
getLocaleEntries(): array
Returns an array containing all the locales present in the translations (e.g., [ "en", "fr" ]
).
getTranslationEntries(): array
Returns an array containing all the translations values (e.g., [ "hi", "salut" ]
).
getLocale(): ?string
Returns the current JsonTranslation
locale, when creating a new JsonTranslation
it's initialized with the default locale or the current request's locale depending on your configuration. This is the locale which resolveTranslation
and __toString
default to.
setLocale(?string $locale): static
Sets the current JsonTranslation
locale. See getLocale
.
getTranslation(string $locale): ?string
Retrieves a translation for a specific locale.
setTranslation(string $locale, ?string $translation): static
Sets a translation for a specific locale. If $translation
is null
it will be deleted.
hasTranslation(string $locale): bool
Checks if a translation exists for a given locale.
deleteTranslation(string $locale): static
Deletes a translation for a specific locale. This can also be done using setTranslation
.
resolveTranslation(?string $locale = null, bool $fallback = true): ?string
Resolves a translation based on the provided locale, with optional fallback. Defaults to the JsonTranslation
current locale, see getLocale
.
__toArray(): array
Alias for getEntries
.
__toString(): string
Resolves a translation for the current JsonTranslation
locale, see getLocale
. If not found it will try to resolve a fallback locale (including the default locale).
__clone()
Clones the current JsonTranslation
instance and enables usage with the clone
keyword. This can be useful to change its reference to notify doctrine that the instance has changed.
Querying and routing
This bundle provides the JsonTranslatedEntityRepositoryTrait
trait to be used with ServiceEntityRepository
to simplify querying entities that have properties managed by JsonTranslation
.
Its main methods return a NativeQuery
which you can resolve :
/**
* Creates a `NativeQuery` to find entities having translations for a specific locale in given properties.
*
* @param string $locale The locale code to check for (e.g., "en")
* @param string[] $properties An array of property names of type `JsonTranslation`
* @param array $propertyOrders Optional ordering criteria (e.g., ['propertyName' => 'ASC']).
* @param int $limit optional result limit (-1 for no limit)
* @param int $offset optional result offset (0 for no offset)
*
* @throws \Exception if property or value is invalid, or DB platform is unsupported
*/
public function queryByJsonTranslationLocale(
string $locale,
array $properties,
array $propertyOrders = [],
int $limit = -1,
int $offset = 0,
): ?NativeQuery
/**
* Creates a `NativeQuery` to find entities by searching for a value within specified `JsonTranslation` properties across all locales.
*
* @param array $propertyValues associative array ['propertyName' => 'searchValue'], where 'propertyName'
* refers to a property of type `JsonTranslation`
* @param array $propertyOrders Optional ordering criteria (e.g., ['propertyName' => 'DESC'])
* @param int $limit optional result limit (-1 for no limit)
* @param int $offset optional result offset (0 for no offset)
*
* @throws \Exception if property or value is invalid, or DB platform is unsupported
*/
public function queryByJsonTranslationValue(
array $propertyValues,
array $propertyOrders = [],
int $limit = -1,
int $offset = 0,
): ?NativeQuery
/**
* Creates a `NativeQuery` to find entities by matching specific values for a given locale within specified `JsonTranslation` properties.
*
* @param string $locale The locale code to target (e.g., "en")
* @param array $propertyValues associative array ['propertyName' => 'searchValue'], where 'propertyName'
* refers to a property of type `JsonTranslation`
* @param array $propertyOrders Optional ordering criteria (e.g., ['propertyName' => 'ASC']).
* @param int $limit optional result limit (-1 for no limit)
* @param int $offset optional result offset (0 for no offset)
*
* @throws \Exception if locale, property, or value is invalid, or DB platform is unsupported
*/
public function queryByJsonTranslationValueForLocale(
string $locale,
array $propertyValues,
array $propertyOrders = [],
int $limit = -1,
int $offset = 0,
): ?NativeQuery
These methods can be used for routing with #[MapEntity]
. For example :
#[Route(path: "{_locale}/posts/{slug}", name: "posts")]
public function posts(
#[MapEntity(
expr: 'repository.queryByJsonTranslationValueForLocale(_locale, { "slug": slug }).getOneOrNullResult()'
)]
Post $post,
): Response {
// ...
}
/** @extends ServiceEntityRepository<Post> */
class PostRepository extends ServiceEntityRepository
{
use JsonTranslatedEntityRepositoryTrait;
// ...
}
If you worry about database query speeds, check your database support for indexing JSON.
JsonTranslationType
Field
This field type is used to render and manage translations stored in a JsonTranslation object. It provides a user interface for editing translations for different locales.
The full list of options defined and inherited by this form type is available running this command in your app:
php bin/console debug:form JsonTranslationType
Basic form type usage
Imagine you have a field called title that stores translations in a JsonTranslation object. You can use JsonTranslationType to provide a user-friendly way to edit these translations:
use AlexandreFernandez\JsonTranslationBundle\Form\JsonTranslationType; // verify you're not importing the doctrine type
$builder->add('title', JsonTranslationType::class, [
"locales" => ["en", "fr", "es"] // optional, defaults to the configuration's `enabled_locales`
]);
Field options
locales
type: array
default: %json_translation.enabled_locales%
An array of locales that should be displayed in the form. If not specified, it defaults to the enabled locales in this bundle's config. The value can't be an empty array.
display_language
type: int
or boolean
default: JsonTranslationType::DISPLAY_LANGUAGE_CURRENT
Determines how the language associated with each translation is displayed. It accepts one of the following integer constants or false :
JsonTranslationType::DISPLAY_LANGUAGE_CURRENT
(0
): Displays the language name using the current request locale (e.g., "English (en)" if the current request is in English).
JsonTranslationType::DISPLAY_LANGUAGE_NATIVE
(1
): Displays the language name in its native language (e.g., "Français (fr)").
JsonTranslationType::DISPLAY_LANGUAGE_DEFAULT
(2
): Displays the language name using the application's default locale.
false
: Does not display the language name next to the locale.
label_to_legend
type: bool
default: true
If set to true
, the form's label will be used as the legend of the fieldset that wraps the translations. The real label is then hidden ($formView->vars["label"]
will be set to false
and $formView->vars["legend"]
will be populated with the label).
named_details
type: bool
default: true
If set to true
, the form's details
elements will have a name
attribute. The name
attribute groups multiple details elements, with only one open at a time.
no_fieldset
type: bool
default: false
If set to true
, the form's fieldset
will be replaced with a div
and label_to_legend
will be set to false
.
Inherited Options
See KeyValueType
.
EasyAdminBundle
You can use the following custom EasyAdmin field to use the default form widget in EasyAdmin :
namespace App\Bundle\EasyAdmin;
use AlexandreFernandez\JsonTranslationBundle\Form\JsonTranslationType;
use AlexandreFernandez\JsonTranslationBundle\Model\JsonTranslation;
use EasyCorp\Bundle\EasyAdminBundle\Contracts\Field\FieldInterface;
use EasyCorp\Bundle\EasyAdminBundle\Field\FieldTrait;
final class JsonTranslationField implements FieldInterface
{
use FieldTrait;
public static function new(string $propertyName, $label = null): self
{
return (new self())
->setProperty($propertyName)
->setLabel($label)
->formatValue(function (JsonTranslation $value) {
return $value->__toString();
})
->setFormType(JsonTranslationType::class)
->setFormTypeOptions(["no_fieldset" => true])
;
}
}
Then in your dashboard add this bundle's form theme to EasyAdmin :
class AdminDashboardController extends AbstractDashboardController
{
// ...
public function configureCrud(): Crud
{
return Crud::new()
->addFormTheme("@JsonTranslation/form/json_translation_layout.html.twig")
;
}
}
You can add the form theme in a specific CRUD controller instead if you do not want to add globally to your admin.