<p>I would say the most cleanest way is to handle all translations via Yii2 <a href="https://www.yiiframework.com/doc/api/2.0/yii-i18n-dbmessagesource" rel="nofollow noreferrer">DbMessageSource</a>.</p>

<p>You could say you want to store all translations into the database instead of files.
There is an <code>MessageSource::EVENT_MISSING_TRANSLATION</code> event in which you can store all translations into your new database table. As soon as Craft recognizes a new translation you'll have the chance to insert the new record</p>

<p>Here is a code snipped from our translation plugin</p>

<pre><code>Event::on(
    MessageSource::class,
    MessageSource::EVENT_MISSING_TRANSLATION,
    function(MissingTranslationEvent $event) {
        if ($event-&gt;message) {
            $message = Translate::getInstance()-&gt;getTranslation()-&gt;addMissingTranslation($event-&gt;category, $event-&gt;message, $event-&gt;language);
            if ($message instanceof Message) {
                $event-&gt;translatedMessage = $message-&gt;translation;
            }
        }
    }
);
</code></pre>

<h3>Your DbMessageSource class could look like this</h3>

<pre><code>class DbMessageSource extends \yii\i18n\DbMessageSource
{
    public $fallback = [];
    public $useMultiSiteTranslationFeature = false;
    protected $fallbackClass;

    /**
     * Return something for everything which is requested
     *
     * @param string $name
     *
     * @return mixed|null
     */
    public function __get($name)
    {
        try {
            return parent::__get($name);
        } catch (\Exception $e) {
            return null;
        }
    }

    /**
     * @param string $name
     * @param mixed  $value
     */
    public function __set($name, $value)
    {
        try {
            parent::__set($name, $value);
        } catch (\Exception $e) {
            $this-&gt;$name = $value;
        }
    }

    public function translateFallback($category, $message, $language)
    {
        if ($this-&gt;useMultiSiteTranslationFeature &amp;&amp; Craft::$app-&gt;getRequest()-&gt;getIsSiteRequest()) {
            $this-&gt;useMultiSiteTranslationFeature = false;
            $translated = $this-&gt;translate($category, $message, $language);
            $this-&gt;useMultiSiteTranslationFeature = true;
            if ($translated) {
                return $translated;
            }
        }
        if (count($this-&gt;fallback) === 0) {
            return false;
        }
        if (!$this-&gt;fallbackClass) {
            $this-&gt;fallbackClass = \Yii::createObject($this-&gt;fallback);
        }

        return $this-&gt;fallbackClass-&gt;translate($category, $message, $language);
    }

    protected function loadMessages($category, $language)
    {
        if ($this-&gt;useMultiSiteTranslationFeature &amp;&amp; Craft::$app-&gt;getRequest()-&gt;getIsSiteRequest()) {
            $category .= '_' . Craft::$app-&gt;getSites()-&gt;currentSite-&gt;handle;
        }

        if ($this-&gt;enableCaching) {
            $key = [
                __CLASS__,
                $category,
                $language,
            ];
            $messages = $this-&gt;cache-&gt;get($key);
            if ($messages === false) {
                $messages = $this-&gt;loadMessagesFromDb($category, $language);
                $this-&gt;cache-&gt;set($key, $messages, $this-&gt;cachingDuration);
            }

            return $messages;
        }

        return $this-&gt;loadMessagesFromDb($category, $language);
    }
}
</code></pre>

<p>The rest is common Craft work to create create a custom form/list for your new CP section or you create an element type and let Craft handle everything.</p>

<p>As you can see in the picture below: all translatable strings from all sources/categories/plugins are recognized and our customers are able to change all translations in all plugins into all languages. Even if the creator of the plugin never wanted to include certain translations into certain languages</p>

<p><a href="https://i.sstatic.net/RO3zz.png" rel="nofollow noreferrer"><img src="https://i.sstatic.net/RO3zz.png" alt="Screenshot"></a></p>

CC BY-SA 4.0
2018-05-02T09:27:36.643Z
Thanks Robin! Any chance you could share the plugin or a simple version of it? I'm not a developer eeeek
James Greig
CC BY-SA 4.0
2018-05-03T11:55:50.913Z
Thanks @robin, got this working (yay) but it only seems to translate the backend dashboard/CP terms, how would you translate the text in a Dropdown field?
James Greig
CC BY-SA 4.0
2018-05-03T12:01:27.207Z
@3stripe `DropDown` fields are translated in the category 'site' (code `'label' => Craft::t('site', $option['label'])`) so you'll have to include the component with setting `site` like stated in the `readme.md`
Robin Schambach
CC BY-SA 3.0
2018-04-30T14:17:34.610Z
2018-04-30T14:28:13.690Z
2018-04-30T14:28:13.690Z
Robin Schambach
<p>check out Enupal Translate, you can enable an option to allow Enupal Translate handle the translations folder of the plugins.</p>

<p><a href="https://enupal.com/en/craft-plugins/translate" rel="nofollow noreferrer">Enupal Translate</a></p>

CC BY-SA 4.0
2018-05-02T21:46:53.827Z
Interesting. But does is overwrite plugins translation files or does it really create a new element for each translation? If so it would be a huge overhead
Robin Schambach
CC BY-SA 4.0
2018-05-02T18:17:58.433Z
2018-05-02T18:17:58.433Z
Andre L.
<p>We have some navigation links being translated via the /translations folder but the client needs the ability to change them too...</p>

<p>Is there a way to make the translations editable from the control panel?</p>

<localization>
Way for clients to edit translation file(s) in control panel?
# Way for clients to edit translation file(s) in control panel?

(Asked by: username_0 on 2018-04-30)

We have some navigation links being translated via the /translations folder but the client needs the ability to change them too...

Is there a way to make the translations editable from the control panel?

**Tags:** localization


---

## 2 answers

### Answer by username_1 on 2018-04-30. Score: 3

I would say the most cleanest way is to handle all translations via Yii2 DbMessageSource.
You could say you want to store all translations into the database instead of files.
There is an MessageSource::EVENT_MISSING_TRANSLATION event in which you can store all translations into your new database table. As soon as Craft recognizes a new translation you'll have the chance to insert the new record
Here is a code snipped from our translation plugin
Event::on(
    MessageSource::class,
    MessageSource::EVENT_MISSING_TRANSLATION,
    function(MissingTranslationEvent $event) {
        if ($event->message) {
            $message = Translate::getInstance()->getTranslation()->addMissingTranslation($event->category, $event->message, $event->language);
            if ($message instanceof Message) {
                $event->translatedMessage = $message->translation;
            }
        }
    }
);

Your DbMessageSource class could look like this
class DbMessageSource extends \yii\i18n\DbMessageSource
{
    public $fallback = [];
    public $useMultiSiteTranslationFeature = false;
    protected $fallbackClass;

    /**
     * Return something for everything which is requested
     *
     * @param string $name
     *
     * @return mixed|null
     */
    public function __get($name)
    {
        try {
            return parent::__get($name);
        } catch (\Exception $e) {
            return null;
        }
    }

    /**
     * @param string $name
     * @param mixed  $value
     */
    public function __set($name, $value)
    {
        try {
            parent::__set($name, $value);
        } catch (\Exception $e) {
            $this->$name = $value;
        }
    }

    public function translateFallback($category, $message, $language)
    {
        if ($this->useMultiSiteTranslationFeature && Craft::$app->getRequest()->getIsSiteRequest()) {
            $this->useMultiSiteTranslationFeature = false;
            $translated = $this->translate($category, $message, $language);
            $this->useMultiSiteTranslationFeature = true;
            if ($translated) {
                return $translated;
            }
        }
        if (count($this->fallback) === 0) {
            return false;
        }
        if (!$this->fallbackClass) {
            $this->fallbackClass = \Yii::createObject($this->fallback);
        }

        return $this->fallbackClass->translate($category, $message, $language);
    }

    protected function loadMessages($category, $language)
    {
        if ($this->useMultiSiteTranslationFeature && Craft::$app->getRequest()->getIsSiteRequest()) {
            $category .= '_' . Craft::$app->getSites()->currentSite->handle;
        }

        if ($this->enableCaching) {
            $key = [
                __CLASS__,
                $category,
                $language,
            ];
            $messages = $this->cache->get($key);
            if ($messages === false) {
                $messages = $this->loadMessagesFromDb($category, $language);
                $this->cache->set($key, $messages, $this->cachingDuration);
            }

            return $messages;
        }

        return $this->loadMessagesFromDb($category, $language);
    }
}

The rest is common Craft work to create create a custom form/list for your new CP section or you create an element type and let Craft handle everything.
As you can see in the picture below: all translatable strings from all sources/categories/plugins are recognized and our customers are able to change all translations in all plugins into all languages. Even if the creator of the plugin never wanted to include certain translations into certain languages

> **Comments:**
> **username_0**: Thanks Robin! Any chance you could share the plugin or a simple version of it? I'm not a developer eeeek  
> **username_0**: Thanks @robin, got this working (yay) but it only seems to translate the backend dashboard/CP terms, how would you translate the text in a Dropdown field?  
> **username_1**: @3stripe `DropDown` fields are translated in the category 'site' (code `'label' => Craft::t('site', $option['label'])`) so you'll have to include the component with setting `site` like stated in the `readme.md`  

---

### Answer by username_2 on 2018-05-02. Score: 0

check out Enupal Translate, you can enable an option to allow Enupal Translate handle the translations folder of the plugins.

[Enupal Translate](https://enupal.com/en/craft-plugins/translate)

> **Comments:**
> **username_1**: Interesting. But does is overwrite plugins translation files or does it really create a new element for each translation? If so it would be a huge overhead  

---

