Way for clients to edit translation file(s) in control panel? 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?
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-&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;
            }
        }
    }
);


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-&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);
    }
}


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