discord-translate

Translation utilities for discord.

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         discord-translate
// @namespace    http://tampermonkey.net/
// @version      2025-07-01
// @description  Translation utilities for discord.
// @author       Patrick Learn
// @match        https://discord.com/channels/*
// @grant        GM_xmlhttpRequest
// @grant        GM_setValue
// @grant        GM_getValue
// @license MIT
// ==/UserScript==

(async () => {
  "use strict";

  const CONFIG = {
    async load() {
      this.translationEngine = await GM_getValue("translationEngine", "llm");
      this.llmApiKey = await GM_getValue("llmApiKey", "YOUR_API_KEY");
      this.llmApiEndpoint = await GM_getValue(
        "llmApiEndpoint",
        "https://api.deepseek.com/v1/chat/completions"
      );
      this.llmModel = await GM_getValue("llmModel", "deepseek-chat");
      this.llmChannelInstructions = await GM_getValue(
        "llmChannelInstructions",
        "You are a professional translator. Translate to English. Do not use markdown. Keep it minimal. You may include some translation notes. Do not ask me questions."
      );
      this.llmInputBarInstructions = await GM_getValue(
        "llmInputBarInstructions",
        "You are a professional translator. Translate to Danish. Do not include anything extra, just write it as it should be."
      );
      this.libreTranslateEndpoint = await GM_getValue(
        "libreTranslateEndpoint",
        "https://libretranslate.com/translate"
      );
      this.libreChannelFromLang = await GM_getValue("libreChannelFromLang", "auto");
      this.libreChannelToLang = await GM_getValue("libreChannelToLang", "en");
      this.libreInputBarFromLang = await GM_getValue("libreInputBarFromLang", "auto");
      this.libreInputBarToLang = await GM_getValue("libreInputBarToLang", "da");
      this.deeplApiKey = await GM_getValue("deeplApiKey", "YOUR_API_KEY");
      this.deeplApiEndpoint = await GM_getValue(
        "deeplApiEndpoint",
        "https://api.deepl.com/v2/translate"
      );
      this.deeplChannelFromLang = await GM_getValue("deeplChannelFromLang", "auto");
      this.deeplChannelToLang = await GM_getValue("deeplChannelToLang", "EN");
      this.deeplInputBarFromLang = await GM_getValue("deeplInputBarFromLang", "auto");
      this.deeplInputBarToLang = await GM_getValue("deeplInputBarToLang", "DA");
    },
    async save(key, value) {
      await GM_setValue(key, value);
      this[key] = value;
    },
  };

  const Translator = (() => {
    const MODE = Object.freeze({ CHANNEL: 0, INPUTBAR: 1 });

    function gmRequest({ url, method = "POST", headers = {}, body }) {
      return new Promise((resolve, reject) => {
        GM_xmlhttpRequest({
          method,
          url,
          headers,
          data: body,
          onload: resolve,
          onerror: reject,
        });
      });
    }

    async function translateWithLLM(text, mode) {
      const url = CONFIG.llmApiEndpoint;
      const headers = {
        "Content-Type": "application/json",
        Authorization: `Bearer ${CONFIG.llmApiKey}`,
      };

      const instructions =
        mode === Translator.MODE.CHANNEL
          ? CONFIG.llmChannelInstructions
          : CONFIG.llmInputBarInstructions;

      const body = JSON.stringify({
        model: CONFIG.llmModel,
        messages: [
          { role: "system", content: instructions },
          { role: "user", content: text },
        ],
        temperature: 0.2,
      });

      const resp = await gmRequest({ url, headers, body });
      if (resp.status < 200 || resp.status >= 300) {
        throw new Error(`LLM HTTP ${resp.status}`);
      }

      const data = JSON.parse(resp.responseText);
      const content = data.choices?.[0]?.message?.content;
      if (!content) throw new Error("Unexpected LLM response");
      return content.trim();
    }

    async function translateWithLibre(text, mode) {
      const url = CONFIG.libreTranslateEndpoint;
      const headers = { "Content-Type": "application/json" };

      const source =
        mode === Translator.MODE.CHANNEL
          ? CONFIG.libreChannelFromLang
          : CONFIG.libreInputBarFromLang;

      const target =
        mode === Translator.MODE.CHANNEL
          ? CONFIG.libreChannelToLang
          : CONFIG.libreInputBarToLang;

      const body = JSON.stringify({
        q: text,
        source,
        target,
        format: "text",
      });

      const resp = await gmRequest({ url, headers, body });
      if (resp.status < 200 || resp.status >= 300) {
        throw new Error(`Libre HTTP ${resp.status}`);
      }

      const data = JSON.parse(resp.responseText);
      if (!data.translatedText) throw new Error("Unexpected Libre response");
      return data.translatedText.trim();
    }

    async function translateWithDeepL(text, mode) {
      const url = CONFIG.deeplApiEndpoint;

      const source =
        mode === Translator.MODE.CHANNEL
          ? CONFIG.deeplChannelFromLang
          : CONFIG.deeplInputBarFromLang;

      const target =
        mode === Translator.MODE.CHANNEL
          ? CONFIG.deeplChannelToLang
          : CONFIG.deeplInputBarToLang;

      const body = JSON.stringify({
        text: [text],
        target_lang: target,
        ...(source && source.toLowerCase() !== "auto" ? { source_lang: source } : {}),
      });

      const headers = {
        "Content-Type": "application/json",
        Authorization: `DeepL-Auth-Key ${CONFIG.deeplApiKey}`,
      };

      console.log("DeepL request payload:", JSON.parse(body));
      console.log("DeepL request headers:", headers);

      const resp = await gmRequest({ url, method: "POST", headers, body });
      if (resp.status < 200 || resp.status >= 300) {
        throw new Error(`DeepL HTTP ${resp.status}`);
      }

      const data = JSON.parse(resp.responseText);
      if (!data.translations || !data.translations[0]?.text) {
        throw new Error("Unexpected DeepL response");
      }
      return data.translations[0].text.trim();
    }

    async function translate(text, mode = Translator.MODE.INPUTBAR) {
      if (mode !== Translator.MODE.CHANNEL && mode !== Translator.MODE.INPUTBAR) {
        throw new Error(`Invalid mode: ${mode}`);
      }

      try {
        switch (CONFIG.translationEngine) {
          case "llm":
            return await translateWithLLM(text, mode);

          case "libre":
            return await translateWithLibre(text, mode);

          case "deepl":
            return await translateWithDeepL(text, mode);

          default:
            throw new Error(`Unknown engine: ${CONFIG.translationEngine}`);
        }
      } catch (err) {
        console.error("Translation failed:", err);
        alert(`Translation failed: ${err.message}`);
        throw err;
      }
    }

    return { translate, MODE };
  })();

  const UI = (() => {
    const translateSVG = `
            <svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" role="img" width="24" height="24" fill="currentColor" viewBox="0 0 512 512">
                <path d="M478.33,433.6l-90-218a22,22,0,0,0-40.67,0l-90,218a22,22,0,1,0,40.67,16.79L316.66,406H419.33l18.33,44.39A22,22,0,0,0,458,464a22,22,0,0,0,20.32-30.4ZM334.83,362,368,281.65,401.17,362Z"/>
                <path d="M267.84,342.92a22,22,0,0,0-4.89-30.7c-.2-.15-15-11.13-36.49-34.73,39.65-53.68,62.11-114.75,71.27-143.49H330a22,22,0,0,0,0-44H214V70a22,22,0,0,0-44,0V90H54a22,22,0,0,0,0,44H251.25c-9.52,26.95-27.05,69.5-53.79,108.36-31.41-41.68-43.08-68.65-43.17-68.87a22,22,0,0,0-40.58,17c.58,1.38,14.55,34.23,52.86,83.93.92,1.19,1.83,2.35,2.74,3.51-39.24,44.35-77.74,71.86-93.85,80.74a22,22,0,1,0,21.07,38.63c2.16-1.18,48.6-26.89,101.63-85.59,22.52,24.08,38,35.44,38.93,36.1a22,22,0,0,0,30.75-4.9Z"/>
            </svg>
        `;

    function openConfigPanel() {
      if (document.getElementById("config-panel")) return;

      const panel = document.createElement("div");
      panel.id = "config-panel";
      panel.style.position = "fixed";
      panel.style.top = "50px";
      panel.style.right = "50px";
      panel.style.zIndex = "9999";
      panel.style.background = "#2f3136";
      panel.style.color = "#fff";
      panel.style.padding = "20px";
      panel.style.border = "1px solid #555";
      panel.style.borderRadius = "8px";
      panel.style.boxShadow = "0 0 10px rgba(0,0,0,0.5)";
      panel.style.width = "400px";

      panel.innerHTML = `
                <label>Translation Engine:<br>
                <select id="cfg-translationEngine" style="width:100%">
                    <option value="llm" ${
                      CONFIG.translationEngine === "llm" ? "selected" : ""
                    }>LLM</option>
                    <option value="libre" ${
                      CONFIG.translationEngine === "libre" ? "selected" : ""
                    }>LibreTranslate</option>
                    <option value="deepl" ${
                      CONFIG.translationEngine === "deepl" ? "selected" : ""
                    }>DeepL</option>
                </select>
                </label><br><br>
                <hr style="border-color:#555;"><br>
                <div id="config-section"></div>
                <button id="cfg-save" style="margin-right:10px">Save</button>
                <button id="cfg-close">Close</button>
            `;

      document.body.appendChild(panel);

      const configSection = panel.querySelector("#config-section");

      const llmConfig = `
                <label><strong>API Configuration:</strong></label><br><br>
                <label>API Key:<br>
                    <input type="password" id="cfg-llmApiKey" value="${CONFIG.llmApiKey}" style="width:100%">
                </label><br><br>
                <label>Endpoint:<br>
                    <input type="text" id="cfg-llmApiEndpoint" value="${CONFIG.llmApiEndpoint}" style="width:100%">
                </label><br><br>
                <label>Model:<br>
                    <input type="text" id="cfg-llmModel" value="${CONFIG.llmModel}" style="width:100%">
                </label><br><br>
                <hr style="border-color:#555;"><br>
                <label><strong>Instructions:</strong></label><br><br>
                <label>Channel Instructions (Reading):<br>
                    <textarea id="cfg-llmChannelInstructions" style="width:100%" rows="2">${CONFIG.llmChannelInstructions}</textarea>
                </label><br><br>
                <label>Input Bar Instructions (Writing):<br>
                    <textarea id="cfg-llmInputBarInstructions" style="width:100%" rows="2">${CONFIG.llmInputBarInstructions}</textarea>
                </label><br><br>
            `;

      const libreConfig = `
                <label><strong>Endpoint:</strong></label><br><br>
                <label>Endpoint URL:<br>
                    <input type="text" id="cfg-libreTranslateEndpoint" value="${CONFIG.libreTranslateEndpoint}" style="width:100%">
                </label><br><br>
                <hr style="border-color:#555;"><br>
                <label><strong>Channel Translation (Reading):</strong></label><br><br>
                <label>Source Language:<br>
                    <input type="text" id="cfg-libreChannelFromLang" value="${CONFIG.libreChannelFromLang}" style="width:100%">
                </label><br><br>
                <label>Target Language:<br>
                    <input type="text" id="cfg-libreChannelToLang" value="${CONFIG.libreChannelToLang}" style="width:100%">
                </label><br><br>
                <hr style="border-color:#555;"><br>
                <label><strong>Input Bar Translation (Writing):</strong></label><br><br>
                <label>Source Language:<br>
                    <input type="text" id="cfg-libreInputBarFromLang" value="${CONFIG.libreInputBarFromLang}" style="width:100%">
                </label><br><br>
                <label>Target Language:<br>
                    <input type="text" id="cfg-libreInputBarToLang" value="${CONFIG.libreInputBarToLang}" style="width:100%">
                </label><br><br>
            `;

      const deeplConfig = `
                <label><strong>API Configuration:</strong></label><br><br>
                <label>DeepL API Key:<br>
                    <input type="password" id="cfg-deeplApiKey" value="${CONFIG.deeplApiKey}" style="width:100%">
                </label><br><br>
                <label>DeepL Endpoint:<br>
                    <input type="text" id="cfg-deeplApiEndpoint" value="${CONFIG.deeplApiEndpoint}" style="width:100%">
                </label><br><br>
                <hr style="border-color:#555;"><br>
                <label><strong>Channel Translation (Reading):</strong></label><br><br>
                <label>Source Language:<br>
                    <input type="text" id="cfg-deeplChannelFromLang" value="${CONFIG.deeplChannelFromLang}" style="width:100%">
                </label><br><br>
                <label>Target Language:<br>
                    <input type="text" id="cfg-deeplChannelToLang" value="${CONFIG.deeplChannelToLang}" style="width:100%">
                </label><br><br>
                <hr style="border-color:#555;"><br>
                <label><strong>Input Bar Translation (Writing):</strong></label><br><br>
                <label>Source Language:<br>
                    <input type="text" id="cfg-deeplInputBarFromLang" value="${CONFIG.deeplInputBarFromLang}" style="width:100%">
                </label><br><br>
                <label>Target Language:<br>
                    <input type="text" id="cfg-deeplInputBarToLang" value="${CONFIG.deeplInputBarToLang}" style="width:100%">
                </label><br><br>
            `;

      function updateConfigSection() {
        const selectedEngine = document.getElementById("cfg-translationEngine").value;
        if (selectedEngine === "llm") {
          configSection.innerHTML = llmConfig;
        } else if (selectedEngine === "libre") {
          configSection.innerHTML = libreConfig;
        } else if (selectedEngine === "deepl") {
          configSection.innerHTML = deeplConfig;
        } else {
          configSection.innerHTML = "<p>Unsupported engine selected.</p>";
        }
      }

      updateConfigSection();

      document
        .getElementById("cfg-translationEngine")
        .addEventListener("change", updateConfigSection);

      document.getElementById("cfg-save").addEventListener("click", async () => {
        const selectedEngine = document.getElementById("cfg-translationEngine").value;
        await CONFIG.save("translationEngine", selectedEngine);

        if (selectedEngine === "llm") {
          await CONFIG.save(
            "llmApiKey",
            document.getElementById("cfg-llmApiKey").value
          );
          await CONFIG.save(
            "llmApiEndpoint",
            document.getElementById("cfg-llmApiEndpoint").value
          );
          await CONFIG.save("llmModel", document.getElementById("cfg-llmModel").value);
          await CONFIG.save(
            "llmChannelInstructions",
            document.getElementById("cfg-llmChannelInstructions").value
          );
          await CONFIG.save(
            "llmInputBarInstructions",
            document.getElementById("cfg-llmInputBarInstructions").value
          );
        } else if (selectedEngine === "libre") {
          await CONFIG.save(
            "libreTranslateEndpoint",
            document.getElementById("cfg-libreTranslateEndpoint").value
          );
          await CONFIG.save(
            "libreChannelFromLang",
            document.getElementById("cfg-libreChannelFromLang").value
          );
          await CONFIG.save(
            "libreChannelToLang",
            document.getElementById("cfg-libreChannelToLang").value
          );
          await CONFIG.save(
            "libreInputBarFromLang",
            document.getElementById("cfg-libreInputBarFromLang").value
          );
          await CONFIG.save(
            "libreInputBarToLang",
            document.getElementById("cfg-libreInputBarToLang").value
          );
        } else if (selectedEngine === "deepl") {
          await CONFIG.save(
            "deeplApiKey",
            document.getElementById("cfg-deeplApiKey").value
          );
          await CONFIG.save(
            "deeplApiEndpoint",
            document.getElementById("cfg-deeplApiEndpoint").value
          );
          await CONFIG.save(
            "deeplChannelFromLang",
            document.getElementById("cfg-deeplChannelFromLang").value
          );
          await CONFIG.save(
            "deeplChannelToLang",
            document.getElementById("cfg-deeplChannelToLang").value
          );
          await CONFIG.save(
            "deeplInputBarFromLang",
            document.getElementById("cfg-deeplInputBarFromLang").value
          );
          await CONFIG.save(
            "deeplInputBarToLang",
            document.getElementById("cfg-deeplInputBarToLang").value
          );
        }

        panel.remove();
      });

      document
        .getElementById("cfg-close")
        .addEventListener("click", () => panel.remove());
    }

    function createHeaderButton() {
      const button = document.createElement("button");
      button.id = "my-custom-header-button";
      button.style.background = "transparent";
      button.style.border = "none";
      button.style.cursor = "pointer";
      button.style.padding = "4px";
      button.style.display = "flex";
      button.style.alignItems = "center";
      button.style.justifyContent = "center";
      button.style.color = "#ffffff";
      button.innerHTML = `
                <div class="contents__201d5 button__24af7 button__74017">
                    <div class="buttonWrapper__24af7">
                        ${translateSVG}
                    </div>
                </div>
            `;
      button.addEventListener("click", () => {
        openConfigPanel();
      });
      return button;
    }

    function createMessageButton() {
      const button = document.createElement("div");
      button.classList.add("hoverBarButton_f84418", "button_f7ecac");
      button.setAttribute("aria-label", "Translate message");
      button.setAttribute("role", "button");
      button.setAttribute("tabindex", "0");
      button.innerHTML = `
                <div class="contents__201d5 button__24af7 button__74017">
                    <div class="buttonWrapper__24af7">
                        ${translateSVG}
                    </div>
                </div>
            `;
      button.addEventListener("click", async () => {
        const messageContainer = button.closest(".message__5126c");
        if (!messageContainer) {
          alert("Could not find the message container.");
          return;
        }

        const contentElements = messageContainer.querySelectorAll(
          ".messageContent_c19a55"
        );
        if (contentElements.length === 0) {
          alert("Could not find any message text.");
          return;
        }

        const contentElement = contentElements[contentElements.length - 1];
        const textToTranslate = contentElement.innerText || contentElement.textContent;
        if (!textToTranslate) {
          alert("No text found to translate.");
          return;
        }

        console.log("Translating message text:", textToTranslate);

        const originalButtonHTML = button.innerHTML;
        button.innerHTML = `<svg width="24" height="24" viewBox="0 0 100 100" fill="#9aff99" xmlns="http://www.w3.org/2000/svg">
                    <circle cx="50" cy="50" r="35" stroke="currentColor" stroke-width="10" fill="none" stroke-dasharray="164.93361431346415" stroke-dashoffset="164.93361431346415">
                        <animateTransform attributeName="transform" type="rotate" repeatCount="indefinite" dur="1s" values="0 50 50;360 50 50" keyTimes="0;1"/>
                        <animate attributeName="stroke-dashoffset" repeatCount="indefinite" dur="1s" values="164.93361431346415;0" keyTimes="0;1"/>
                    </circle>
                </svg>`;

        try {
          const translated = await Translator.translate(
            textToTranslate,
            Translator.MODE.CHANNEL
          );

          if (translated) {
            const oldTranslation = messageContainer.querySelector(".my-translation");
            if (oldTranslation) oldTranslation.remove();

            const separator = document.createElement("div");
            separator.className = "my-translation-separator";
            separator.style.marginTop = "5px";
            separator.style.borderTop = "1px dashed rgba(255, 255, 255, 0.2)";
            separator.style.height = "1px";
            separator.style.width = "100%";

            const translationDiv = document.createElement("div");
            translationDiv.className = "my-translation";
            translationDiv.style.marginTop = "5px";
            translationDiv.style.opacity = "0.9";
            translationDiv.style.color = "#9aff99";
            translationDiv.innerText = translated;

            contentElement.appendChild(separator);
            contentElement.appendChild(translationDiv);
          }
        } catch (error) {
          console.error("Translation failed:", error);
        } finally {
          button.innerHTML = originalButtonHTML;
        }
      });

      return button;
    }

    function createInputBarButton() {
      const button = document.createElement("button");
      button.id = "my-custom-input-button";
      button.type = "button";
      button.setAttribute("aria-label", "Translate input");
      button.className = "button__201d5 lookBlank__201d5 colorBrand__201d5 grow__201d5";
      button.style.display = "flex";
      button.style.alignItems = "center";
      button.style.justifyContent = "center";
      button.style.background = "transparent";
      button.style.border = "none";
      button.style.cursor = "pointer";
      button.style.padding = "4px";

      button.innerHTML = `
                <div class="contents__201d5 button__24af7 button__74017">
                    <div class="buttonWrapper__24af7">
                        ${translateSVG}
                    </div>
                </div>
            `;

      button.addEventListener("click", async () => {
        const textArea = document.querySelector(".textArea__74017");
        if (!textArea) {
          alert("No text area found.");
          return;
        }

        const currentText = textArea.value || textArea.textContent;
        if (!currentText) {
          alert("No text to translate!");
          return;
        }

        console.log("Translating input text:", currentText);

        const originalButtonHTML = button.innerHTML;
        button.innerHTML = `<svg width="24" height="24" viewBox="0 0 100 100" fill="#9aff99" xmlns="http://www.w3.org/2000/svg">
                    <circle cx="50" cy="50" r="35" stroke="#ffff" stroke-width="10" fill="none" stroke-dasharray="164.93361431346415" stroke-dashoffset="164.93361431346415">
                        <animateTransform attributeName="transform" type="rotate" repeatCount="indefinite" dur="1s" values="0 50 50;360 50 50" keyTimes="0;1"/>
                        <animate attributeName="stroke-dashoffset" repeatCount="indefinite" dur="1s" values="164.93361431346415;0" keyTimes="0;1"/>
                    </circle>
                </svg>`;

        try {
          const translated = await Translator.translate(
            currentText,
            Translator.MODE.INPUTBAR
          );

          if (translated) {
            try {
              await navigator.clipboard.writeText(translated);
              const notification = document.createElement("div");
              notification.innerText = "Translation copied! Press Ctrl+V to paste.";
              notification.style.position = "fixed";
              notification.style.bottom = "20px";
              notification.style.right = "20px";
              notification.style.background = "#2f3136";
              notification.style.color = "#fff";
              notification.style.padding = "10px 15px";
              notification.style.borderRadius = "6px";
              notification.style.boxShadow = "0 2px 10px rgba(0,0,0,0.4)";
              notification.style.zIndex = "9999";
              document.body.appendChild(notification);

              setTimeout(() => {
                notification.remove();
              }, 3000);
            } catch (error) {
              console.error("Clipboard copy failed:", error);
              alert("Failed to copy translation. Please try manually.");
            }
          }
        } catch (error) {
          console.error("Translation failed:", error);
        } finally {
          button.innerHTML = originalButtonHTML;
        }
      });

      return button;
    }

    function watchMessages() {
      const observer = new MutationObserver(() => {
        // Channel header
        const header = document.querySelector(
          'section[aria-label="Channel header"] .toolbar__9293f'
        );
        if (header && !document.getElementById("my-custom-header-button")) {
          header.appendChild(createHeaderButton());
        }

        // Message hover toolbars
        const buttonsContainers = document.querySelectorAll(
          ".buttonsInner__5126c:not(.has-translate-button)"
        );
        buttonsContainers.forEach((container) => {
          container.classList.add("has-translate-button");
          const messageButton = createMessageButton();

          const separators = container.querySelectorAll(".separator_f84418");
          const lastSeparator = separators[separators.length - 1];

          container.insertBefore(
            messageButton,
            lastSeparator ? lastSeparator.nextSibling : null
          );
        });

        // Input bar
        const inputBarButtons = document.querySelector(".buttons__74017");
        if (inputBarButtons && !document.getElementById("my-custom-input-button")) {
          const inputButton = createInputBarButton();
          if (inputBarButtons.firstChild) {
            inputBarButtons.insertBefore(inputButton, inputBarButtons.firstChild);
          } else {
            inputBarButtons.appendChild(inputButton);
          }
        }
      });

      observer.observe(document.body, { childList: true, subtree: true });
    }

    function init() {
      watchMessages();
    }

    return { init };
  })();

  await CONFIG.load();
  UI.init();
})();