model = $model; $this->initTranslatableContext(); $this->model->bindEvent('model.beforeGetAttribute', function ($key) use ($model) { if ($this->isTranslatable($key)) { $value = $this->getAttributeTranslated($key); if ($model->hasGetMutator($key)) { $method = 'get' . Str::studly($key) . 'Attribute'; $value = $model->{$method}($value); } return $value; } }); $this->model->bindEvent('model.beforeSetAttribute', function ($key, $value) use ($model) { if ($this->isTranslatable($key)) { $value = $this->setAttributeTranslated($key, $value); if ($model->hasSetMutator($key)) { $method = 'set' . Str::studly($key) . 'Attribute'; $value = $model->{$method}($value); } return $value; } }); $this->model->bindEvent('model.saveInternal', function() { $this->syncTranslatableAttributes(); }); } /** * Initializes this class, sets the default language code to use. * @return void */ public function initTranslatableContext() { $translate = Translator::instance(); $this->translatableContext = $translate->getLocale(); $this->translatableDefault = $translate->getDefaultLocale(); } /** * Checks if an attribute should be translated or not. * @param string $key * @return boolean */ public function isTranslatable($key) { if ($key === 'translatable' || $this->translatableDefault == $this->translatableContext) { return false; } return in_array($key, $this->model->getTranslatableAttributes()); } /** * Disables translation fallback locale. * @return self */ public function noFallbackLocale() { $this->translatableUseFallback = false; return $this->model; } /** * Enables translation fallback locale. * @return self */ public function withFallbackLocale() { $this->translatableUseFallback = true; return $this->model; } /** * Returns a translated attribute value. * * The base value must come from 'attributes' on the model otherwise the process * can possibly loop back to this event, then method triggered by __get() magic. * * @param string $key * @param string $locale * @return string */ public function getAttributeTranslated($key, $locale = null) { if ($locale == null) { $locale = $this->translatableContext; } /* * Result should not return NULL to successfully hook beforeGetAttribute event */ $result = ''; /* * Default locale */ if ($locale == $this->translatableDefault) { $result = $this->getAttributeFromData($this->model->attributes, $key); } /* * Other locale */ else { if (!array_key_exists($locale, $this->translatableAttributes)) { $this->loadTranslatableData($locale); } if ($this->hasTranslation($key, $locale)) { $result = $this->getAttributeFromData($this->translatableAttributes[$locale], $key); } elseif ($this->translatableUseFallback) { $result = $this->getAttributeFromData($this->model->attributes, $key); } } /* * Handle jsonable attributes, default locale may return the value as a string */ if ( is_string($result) && method_exists($this->model, 'isJsonable') && $this->model->isJsonable($key) ) { $result = json_decode($result, true); } return $result; } /** * Returns all translated attribute values. * @param string $locale * @return array */ public function getTranslateAttributes($locale) { if (!array_key_exists($locale, $this->translatableAttributes)) { $this->loadTranslatableData($locale); } return array_get($this->translatableAttributes, $locale, []); } /** * Returns whether the attribute is translatable (has a translation) for the given locale. * @param string $key * @param string $locale * @return bool */ public function hasTranslation($key, $locale) { /* * If the default locale is passed, the attributes are retreived from the model, * otherwise fetch the attributes from the $translatableAttributes property */ if ($locale == $this->translatableDefault) { $translatableAttributes = $this->model->attributes; } else { /* * Ensure that the translatableData has been loaded * @see https://github.com/rainlab/translate-plugin/issues/302 */ if (!isset($this->translatableAttributes[$locale])) { $this->loadTranslatableData($locale); } $translatableAttributes = $this->translatableAttributes[$locale]; } return !!$this->getAttributeFromData($translatableAttributes, $key); } /** * Sets a translated attribute value. * @param string $key Attribute * @param string $value Value to translate * @return string Translated value */ public function setAttributeTranslated($key, $value, $locale = null) { if ($locale == null) { $locale = $this->translatableContext; } if ($locale == $this->translatableDefault) { return $this->setAttributeFromData($this->model->attributes, $key, $value); } if (!array_key_exists($locale, $this->translatableAttributes)) { $this->loadTranslatableData($locale); } return $this->setAttributeFromData($this->translatableAttributes[$locale], $key, $value); } /** * Restores the default language values on the model and * stores the translated values in the attributes table. * @return void */ public function syncTranslatableAttributes() { /* * Spin through the known locales, store the translations if necessary */ $knownLocales = array_keys($this->translatableAttributes); foreach ($knownLocales as $locale) { if (!$this->isTranslateDirty(null, $locale)) { continue; } $this->storeTranslatableData($locale); } /* * Saving the default locale, no need to restore anything */ if ($this->translatableContext == $this->translatableDefault) { return; } /* * Restore translatable values to models originals */ $original = $this->model->getOriginal(); $attributes = $this->model->getAttributes(); $translatable = $this->model->getTranslatableAttributes(); $originalValues = array_intersect_key($original, array_flip($translatable)); $this->model->attributes = array_merge($attributes, $originalValues); } /** * Changes the active language for this model * @param string $context * @return void */ public function translateContext($context = null) { if ($context === null) { return $this->translatableContext; } $this->translatableContext = $context; } /** * Shorthand for translateContext method, and chainable. * @param string $context * @return self */ public function lang($context = null) { $this->translateContext($context); return $this->model; } /** * Checks if this model has transatable attributes. * @return true */ public function hasTranslatableAttributes() { return is_array($this->model->translatable) && count($this->model->translatable) > 0; } /** * Returns a collection of fields that will be hashed. * @return array */ public function getTranslatableAttributes() { $translatable = []; if (!is_array($this->model->translatable)) { return []; } foreach ($this->model->translatable as $attribute) { $translatable[] = is_array($attribute) ? array_shift($attribute) : $attribute; } return $translatable; } /** * Returns the defined options for a translatable attribute. * @return array */ public function getTranslatableAttributesWithOptions() { $attributes = []; foreach ($this->model->translatable as $options) { if (!is_array($options)) { continue; } $attributeName = array_shift($options); $attributes[$attributeName] = $options; } return $attributes; } /** * Determine if the model or a given translated attribute has been modified. * @param string|null $attribute * @return bool */ public function isTranslateDirty($attribute = null, $locale = null) { $dirty = $this->getTranslateDirty($locale); if (is_null($attribute)) { return count($dirty) > 0; } else { return array_key_exists($attribute, $dirty); } } /** * Get the locales that have changed, if any * * @return array */ public function getDirtyLocales() { $dirtyLocales = []; $knownLocales = array_keys($this->translatableAttributes); foreach ($knownLocales as $locale) { if ($this->isTranslateDirty(null, $locale)) { $dirtyLocales[] = $locale; } } return $dirtyLocales; } /** * Get the original values of the translated attributes. * @param string|null $locale If `null`, the method will get the original data for all locales. * @return array|null Returns locale data as an array, or `null` if an invalid locale is specified. */ public function getTranslatableOriginals($locale = null) { if (!$locale) { return $this->translatableOriginals; } else { return $this->translatableOriginals[$locale] ?? null; } } /** * Get the translated attributes that have been changed since last sync. * @return array */ public function getTranslateDirty($locale = null) { if (!$locale) { $locale = $this->translatableContext; } if (!array_key_exists($locale, $this->translatableAttributes)) { return []; } if (!array_key_exists($locale, $this->translatableOriginals)) { return $this->translatableAttributes[$locale]; // All dirty } $dirty = []; foreach ($this->translatableAttributes[$locale] as $key => $value) { if (!array_key_exists($key, $this->translatableOriginals[$locale])) { $dirty[$key] = $value; } elseif ($value != $this->translatableOriginals[$locale][$key]) { $dirty[$key] = $value; } } return $dirty; } /** * Extracts a attribute from a model/array with nesting support. * @param mixed $data * @param string $attribute * @return mixed */ protected function getAttributeFromData($data, $attribute) { $keyArray = HtmlHelper::nameToArray($attribute); return array_get($data, implode('.', $keyArray)); } /** * Sets an attribute from a model/array with nesting support. * @param mixed $data * @param string $attribute * @return mixed */ protected function setAttributeFromData(&$data, $attribute, $value) { $keyArray = HtmlHelper::nameToArray($attribute); array_set($data, implode('.', $keyArray), $value); return $value; } /** * Saves the translation data for the model. * @param string $locale * @return void */ abstract protected function storeTranslatableData($locale = null); /** * Loads the translation data from the model. * @param string $locale * @return array */ abstract protected function loadTranslatableData($locale = null); }