Itsnotlupus' I18N Support

no budget? no translators? no problem.

目前為 2023-07-22 提交的版本,檢視 最新版本

此腳本不應該直接安裝,它是一個供其他腳本使用的函式庫。欲使用本函式庫,請在腳本 metadata 寫上: // @require https://update.cn-greasyfork.org/scripts/471000/1223925/Itsnotlupus%27%20I18N%20Support.js

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

You will need to install an extension such as Tampermonkey to install this script.

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         Itsnotlupus' I18N Support
// @namespace    Itsnotlupus Industries
// @version      1.2.1
// @description  no budget? no translators? no problem.
// @author       Itsnotlupus
// @license      MIT
// @grant        GM_xmlhttpRequest
// @grant        GM_setValue
// @grant        GM_getValue
// ==/UserScript==

/*jshint esversion: 11 */

/* There are 2 kind of localized strings you'll find in userscripts:
 * 1. strings found in the original web page.
 * 2. strings used by the userscript itself.
 *
 * To get the former translated, you will need to hook into the web app and somehow get a hold of the proper localized strings.
 *
 * For the latter, you can rely on this library to ask Google Translate for some machine-translated strings.  
 * It's not going to be flawless, but it should generally be close enough.  
 * API results are cached locally to keep things friendly with Google.
 *
 * Pass an array for strings found in the original page, and an object mapping ids to strings for your userscript strings.
 *
 * Usage pattern:
 *
 * await i18n.init({
 *    stringsArray: [ <Array of english app strings> ],
 *    stringsObject: { <Object mapping ids to english script settings> },
 *    strings: <either of the above values>,
 *    lang: "<language code ('en','fr','es', etc.)>",
 *    callback: async (stringsArray) => return {<Object mapping english app strings to localized app strings>}
 * });
 *
 * All of the object values are optional. `document.documentElement.lang` will be used as value for `lang` by default.
 * The callback function can be asynchronous and will be awaited correctly.
 * Only pass a callback if you pass an array of string, as it's useless otherwise.  
 * Note that the .init() method is asynchronous and must be awaited before you can start using t`string`.
 *
 * Afterward, you can use t`<english app string>` or t`<script id>` to get a localized string.
 *
 * ( setEnStrings, setLanguage and initLanguage are kept around for backward whateverability, but probably don't use them. )
 *
 * MISSING: templating, pluralization, or really anything difficult. this is just a cheap stop-gap.
 */
 
class I18N {
    constructor() {
        this.translations = GM_getValue("translations", {});
    }
    
    setEnStrings(enStringsArray = [], enStringsObject = {}) {
        this.enStringsArray = enStringsArray;
        this.enStringsObject = enStringsObject;
        this.i18n = { 'en': Object.assign(this.enStringsArray.reduce((o, v) => (o[v]=v,o), {}), this.enStringsObject) };
    }
    
    async setLanguage(lang = document.documentElement.lang, callback = ()=>{}) {
        this.lang = lang;
        if (lang !== 'en') {
            this.i18n[lang] = await callback(this.enStringsArray) ?? {};
             
            return Promise.all(Object.keys(this.i18n.en).filter(k=>!this.i18n[lang][k]).map(async id => {
                this.i18n[lang][id] = await this.getTranslation(this.i18n.en[id], 'en', lang);
            }));
        }
    }
    
    async initLanguage(lang, callback) { return this.setLanguage(lang, callback); }
    
    // one shot setup rather than using the above methods
    async init({ strings, stringsArray, stringsObject, lang = document.documentElement.lang, callback = ()=>{} }) {
        stringsArray = Array.isArray(strings) ? strings : stringsArray ?? [];
        stringsObject = !Array.isArray(strings) && Object(strings) === strings ? strings : stringsObject ?? {};
        this.setEnStrings(stringsArray, stringsObject);
        return this.setLanguage(lang, callback);
    }
    
    async getTranslation(text, from, to) {
        const key = JSON.stringify({text,from,to});
        let translated = this.translations[key];
        if (!translated) {
          const string = await this._getTranslation(text, from, to);
          if (string) {
            this.translations[key] = string;
            GM_setValue("translations", this.translations);
            translated = string;
          } else {
            translated = text;
          }
        }
        return translated;
    }
    
    _getTranslation(text, from, to) {
        return new Promise((r,e) =>
            GM_xmlhttpRequest({
                method: 'POST',
                url: `https://translate.google.com/translate_a/single?client=at&dt=t&dt=rm&dj=1`,
                data: new URLSearchParams({
                  sl: from,
                  tl: to,
                  q: text
                }),
                responseType: "json",
                onload: v => r(v.response.sentences.map(s=>s.trans).join('')),
                onerror: e
              })
            );
    }
    
    translate(s) {
        return this.i18n[this.lang]?.[s] ?? s;
    }
}

const i18n = new I18N();

const t = s => i18n.translate(s);