var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
    function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
    return new (P || (P = Promise))(function (resolve, reject) {
        function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
        function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
        function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
        step((generator = generator.apply(thisArg, _arguments || [])).next());
    });
};
import { AdminForthPlugin, Filters, suggestIfTypo, AdminForthDataTypes } from "adminforth";
import iso6391 from 'iso-639-1';
import path from 'path';
import fs from 'fs-extra';
import chokidar from 'chokidar';
import { AsyncQueue } from '@sapphire/async-queue';
const processFrontendMessagesQueue = new AsyncQueue();
const SLAVIC_PLURAL_EXAMPLES = {
    uk: 'яблук | Яблуко | Яблука | Яблук', // zero | singular | 2-4 | 5+
    bg: 'ябълки | ябълка | ябълки | ябълки', // zero | singular | 2-4 | 5+
    cs: 'jablek | jablko | jablka | jablek', // zero | singular | 2-4 | 5+
    hr: 'jabuka | jabuka | jabuke | jabuka', // zero | singular | 2-4 | 5+
    mk: 'јаболка | јаболко | јаболка | јаболка', // zero | singular | 2-4 | 5+
    pl: 'jabłek | jabłko | jabłka | jabłek', // zero | singular | 2-4 | 5+
    sk: 'jabĺk | jablko | jablká | jabĺk', // zero | singular | 2-4 | 5+
    sl: 'jabolk | jabolko | jabolka | jabolk', // zero | singular | 2-4 | 5+
    sr: 'јабука | јабука | јабуке | јабука', // zero | singular | 2-4 | 5+
    be: 'яблыкаў | яблык | яблыкі | яблыкаў', // zero | singular | 2-4 | 5+
    ru: 'яблок | яблоко | яблока | яблок', // zero | singular | 2-4 | 5+
};
class CachingAdapterMemory {
    constructor() {
        this.cache = {};
    }
    get(key) {
        return __awaiter(this, void 0, void 0, function* () {
            return this.cache[key];
        });
    }
    set(key, value) {
        return __awaiter(this, void 0, void 0, function* () {
            this.cache[key] = value;
        });
    }
    clear(key) {
        return __awaiter(this, void 0, void 0, function* () {
            if (this.cache[key]) {
                delete this.cache[key];
            }
        });
    }
}
class AiTranslateError extends Error {
    constructor(message) {
        super(message);
        this.name = 'AiTranslateError';
    }
}
export default class I18N extends AdminForthPlugin {
    constructor(options) {
        super(options, import.meta.url);
        this.options = options;
        this.cache = new CachingAdapterMemory();
        this.trFieldNames = {};
    }
    computeCompletedFieldValue(record) {
        return __awaiter(this, void 0, void 0, function* () {
            return this.options.supportedLanguages.reduce((acc, lang) => {
                if (lang === 'en') {
                    return acc;
                }
                if (record[this.trFieldNames[lang]]) {
                    if (acc !== '') {
                        acc += '|';
                    }
                    acc += lang;
                }
                return acc;
            }, '');
        });
    }
    updateUntranslatedMenuBadge() {
        return __awaiter(this, void 0, void 0, function* () {
            if (this.menuItemWithBadgeId) {
                const resource = this.adminforth.resource(this.resourceConfig.resourceId);
                const count = yield resource.count([Filters.NEQ(this.options.completedFieldName, this.fullCompleatedFieldValue)]);
                this.adminforth.websocket.publish(`/opentopic/update-menu-badge/${this.menuItemWithBadgeId}`, {
                    badge: count || null
                });
            }
        });
    }
    modifyResourceConfig(adminforth, resourceConfig) {
        const _super = Object.create(null, {
            modifyResourceConfig: { get: () => super.modifyResourceConfig }
        });
        return __awaiter(this, void 0, void 0, function* () {
            var _a, _b;
            _super.modifyResourceConfig.call(this, adminforth, resourceConfig);
            // check each supported language is valid ISO 639-1 code
            this.options.supportedLanguages.forEach((lang) => {
                if (!iso6391.validate(lang)) {
                    throw new Error(`Invalid language code ${lang}, please define valid ISO 639-1 language code (2 lowercase letters)`);
                }
            });
            // find primary key field
            this.primaryKeyFieldName = (_a = resourceConfig.columns.find(c => c.primaryKey)) === null || _a === void 0 ? void 0 : _a.name;
            if (!this.primaryKeyFieldName) {
                throw new Error(`Primary key field not found in resource ${resourceConfig.resourceId}`);
            }
            // parse trFieldNames
            for (const lang of this.options.supportedLanguages) {
                if (lang === 'en') {
                    continue;
                }
                if ((_b = this.options.translationFieldNames) === null || _b === void 0 ? void 0 : _b[lang]) {
                    this.trFieldNames[lang] = this.options.translationFieldNames[lang];
                }
                else {
                    this.trFieldNames[lang] = lang + '_string';
                }
                // find column by name
                const column = resourceConfig.columns.find(c => c.name === this.trFieldNames[lang]);
                if (!column) {
                    throw new Error(`Field ${this.trFieldNames[lang]} not found for storing translation for language ${lang}
           in resource ${resourceConfig.resourceId}, consider adding it to columns or change trFieldNames option to remap it to existing column`);
                }
            }
            this.enFieldName = this.trFieldNames['en'] || 'en_string';
            this.fullCompleatedFieldValue = this.options.supportedLanguages.reduce((acc, lang) => {
                if (lang === 'en') {
                    return acc;
                }
                if (acc !== '') {
                    acc += '|';
                }
                acc += lang;
                return acc;
            }, '');
            // if not enFieldName column is not found, throw error
            const enColumn = resourceConfig.columns.find(c => c.name === this.enFieldName);
            if (!enColumn) {
                throw new Error(`Field ${this.enFieldName} not found column to store english original string in resource ${resourceConfig.resourceId}`);
            }
            enColumn.editReadonly = true;
            // if sourceFieldName defined, check it exists
            if (this.options.sourceFieldName) {
                if (!resourceConfig.columns.find(c => c.name === this.options.sourceFieldName)) {
                    throw new Error(`Field ${this.options.sourceFieldName} not found in resource ${resourceConfig.resourceId}`);
                }
            }
            // if completedFieldName defined, check it exists and should be string
            if (this.options.completedFieldName) {
                const column = resourceConfig.columns.find(c => c.name === this.options.completedFieldName);
                if (!column) {
                    const similar = suggestIfTypo(resourceConfig.columns.map((col) => col.name), this.options.completedFieldName);
                    throw new Error(`Field ${this.options.completedFieldName} not found in resource ${resourceConfig.resourceId}${similar ? `Did you mean '${similar}'?` : ''}`);
                }
                // if showIn is not defined, add it as empty
                column.showIn = [];
                // add virtual field for incomplete
                resourceConfig.columns.unshift({
                    name: 'fully_translated',
                    label: 'Fully translated',
                    virtual: true,
                    showIn: ['list', 'show', 'filter'],
                    type: AdminForthDataTypes.BOOLEAN,
                });
            }
            const compMeta = {
                brandSlug: adminforth.config.customization.brandNameSlug,
                pluginInstanceId: this.pluginInstanceId,
                supportedLanguages: this.options.supportedLanguages.map(lang => ({
                    code: lang,
                    // lang name on on language native name
                    name: iso6391.getNativeName(lang),
                }))
            };
            // add underLogin component
            (adminforth.config.customization.loginPageInjections.underInputs).push({
                file: this.componentPath('LanguageUnderLogin.vue'),
                meta: compMeta
            });
            (adminforth.config.customization.globalInjections.userMenu).push({
                file: this.componentPath('LanguageInUserMenu.vue'),
                meta: compMeta
            });
            // disable create allowedActions for translations
            resourceConfig.options.allowedActions.create = false;
            // add hook on edit of any translation
            resourceConfig.hooks.edit.afterSave.push((_c) => __awaiter(this, [_c], void 0, function* ({ record, oldRecord }) {
                if (oldRecord) {
                    // find lang which changed
                    let langsChanged = [];
                    for (const lang of this.options.supportedLanguages) {
                        if (lang === 'en') {
                            continue;
                        }
                        if (record[this.trFieldNames[lang]] !== oldRecord[this.trFieldNames[lang]]) {
                            langsChanged.push(lang);
                        }
                    }
                    // clear frontend cache for all langsChanged
                    for (const lang of langsChanged) {
                        this.cache.clear(`${this.resourceConfig.resourceId}:frontend:${lang}`);
                    }
                    this.updateUntranslatedMenuBadge();
                }
                // clear frontend cache for all lan
                return { ok: true };
            }));
            // add hook on delete of any translation to reset cache
            resourceConfig.hooks.delete.afterSave.push((_d) => __awaiter(this, [_d], void 0, function* ({ record }) {
                for (const lang of this.options.supportedLanguages) {
                    this.cache.clear(`${this.resourceConfig.resourceId}:frontend:${lang}`);
                    this.cache.clear(`${this.resourceConfig.resourceId}:${record[this.options.categoryFieldName]}:${lang}:${record[this.enFieldName]}`);
                }
                this.updateUntranslatedMenuBadge();
                return { ok: true };
            }));
            if (this.options.completedFieldName) {
                // on show and list add a list hook which will add incomplete field to record if translation is missing for at least one language
                const addIncompleteField = (record) => {
                    // form list of all langs, sorted by alphabet, without en, to get 'al|ro|uk'
                    record.fully_translated = this.fullCompleatedFieldValue === record[this.options.completedFieldName];
                };
                resourceConfig.hooks.list.afterDatasourceResponse.push((_e) => __awaiter(this, [_e], void 0, function* ({ response }) {
                    response.forEach(addIncompleteField);
                    return { ok: true };
                }));
                resourceConfig.hooks.show.afterDatasourceResponse.push((_f) => __awaiter(this, [_f], void 0, function* ({ response }) {
                    addIncompleteField(response.length && response[0]);
                    return { ok: true };
                }));
                // also add edit hook beforeSave to update completedFieldName
                resourceConfig.hooks.edit.beforeSave.push((_g) => __awaiter(this, [_g], void 0, function* ({ record, oldRecord }) {
                    const futureRecord = Object.assign(Object.assign({}, oldRecord), record);
                    const futureCompletedFieldValue = yield this.computeCompletedFieldValue(futureRecord);
                    record[this.options.completedFieldName] = futureCompletedFieldValue;
                    return { ok: true };
                }));
                // add list hook to support filtering by fully_translated virtual field
                resourceConfig.hooks.list.beforeDatasourceRequest.push((_h) => __awaiter(this, [_h], void 0, function* ({ query }) {
                    if (!query.filters || query.filters.length === 0) {
                        query.filters = [];
                    }
                    // get fully_translated field from filter if it is there
                    const fullyTranslatedFilter = query.filters.find((f) => f.field === 'fully_translated');
                    if (fullyTranslatedFilter) {
                        // remove it from filters because it is virtual field
                        query.filters = query.filters.filter((f) => f.field !== 'fully_translated');
                        if (fullyTranslatedFilter.value[0]) {
                            query.filters.push({
                                field: this.options.completedFieldName,
                                value: this.fullCompleatedFieldValue,
                                operator: 'eq',
                            });
                        }
                        else {
                            query.filters.push({
                                field: this.options.completedFieldName,
                                value: this.fullCompleatedFieldValue,
                                operator: 'ne',
                            });
                        }
                    }
                    return { ok: true };
                }));
            }
            // add bulk action
            if (!resourceConfig.options.bulkActions) {
                resourceConfig.options.bulkActions = [];
            }
            if (this.options.completeAdapter) {
                resourceConfig.options.bulkActions.push({
                    label: 'Translate selected',
                    icon: 'flowbite:language-outline',
                    // if optional `confirm` is provided, user will be asked to confirm action
                    confirm: 'Are you sure you want to translate selected items?',
                    state: 'selected',
                    action: (_j) => __awaiter(this, [_j], void 0, function* ({ selectedIds, tr }) {
                        let translatedCount = 0;
                        try {
                            translatedCount = yield this.bulkTranslate({ selectedIds });
                        }
                        catch (e) {
                            if (e instanceof AiTranslateError) {
                                process.env.HEAVY_DEBUG && console.error('🪲⛔ bulkTranslate error', e);
                                return { ok: false, error: e.message };
                            }
                        }
                        process.env.HEAVY_DEBUG && console.log('🪲bulkTranslate done', selectedIds);
                        this.updateUntranslatedMenuBadge();
                        return {
                            ok: true,
                            error: undefined,
                            successMessage: yield tr(`Translated {count} items`, 'backend', {
                                count: translatedCount,
                            }),
                        };
                    })
                });
            }
            ;
            // if there is menu item with resourceId, add .badge function showing number of untranslated strings
            const addBadgeCountToMenuItem = (menuItem) => {
                this.menuItemWithBadgeId = menuItem.itemId;
                menuItem.badge = () => __awaiter(this, void 0, void 0, function* () {
                    const resource = adminforth.resource(menuItem.resourceId);
                    const count = yield resource.count([Filters.NEQ(this.options.completedFieldName, this.fullCompleatedFieldValue)]);
                    return count ? `${count}` : null;
                });
                menuItem.badgeTooltip = 'Untranslated count';
            };
            adminforth.config.menu.forEach((menuItem) => {
                if (menuItem.resourceId === resourceConfig.resourceId) {
                    addBadgeCountToMenuItem(menuItem);
                }
                if (menuItem.children) {
                    menuItem.children.forEach((child) => {
                        if (child.resourceId === resourceConfig.resourceId) {
                            addBadgeCountToMenuItem(child);
                        }
                    });
                }
            });
        });
    }
    // returns translated count
    bulkTranslate(_a) {
        return __awaiter(this, arguments, void 0, function* ({ selectedIds }) {
            const needToTranslateByLang = {};
            const translations = yield this.adminforth.resource(this.resourceConfig.resourceId).list(Filters.IN(this.primaryKeyFieldName, selectedIds));
            for (const lang of this.options.supportedLanguages) {
                if (lang === 'en') {
                    // all strings are in English, no need to translate
                    continue;
                }
                for (const translation of translations) {
                    if (!translation[this.trFieldNames[lang]]) {
                        if (!needToTranslateByLang[lang]) {
                            needToTranslateByLang[lang] = [];
                        }
                        needToTranslateByLang[lang].push({
                            'en_string': translation[this.enFieldName],
                            category: translation[this.options.categoryFieldName],
                        });
                    }
                }
            }
            const maxKeysInOneReq = 10;
            const updateStrings = {};
            const translateToLang = (langIsoCode_1, strings_1, ...args_1) => __awaiter(this, [langIsoCode_1, strings_1, ...args_1], void 0, function* (langIsoCode, strings, plurals = false) {
                if (strings.length === 0) {
                    return [];
                }
                if (strings.length > maxKeysInOneReq) {
                    let totalTranslated = [];
                    for (let i = 0; i < strings.length; i += maxKeysInOneReq) {
                        const slicedStrings = strings.slice(i, i + maxKeysInOneReq);
                        process.env.HEAVY_DEBUG && console.log('🪲🔪slicedStrings len', slicedStrings.length);
                        const madeKeys = yield translateToLang(langIsoCode, slicedStrings, plurals);
                        totalTranslated = totalTranslated.concat(madeKeys);
                    }
                    return totalTranslated;
                }
                const lang = langIsoCode;
                const requestSlavicPlurals = Object.keys(SLAVIC_PLURAL_EXAMPLES).includes(lang) && plurals;
                const prompt = `
I need to translate strings in JSON to ${lang} language from English for my web app.
${requestSlavicPlurals ? `You should provide 4 translations (in format zero | singular | 2-4 | 5+) e.g. ${SLAVIC_PLURAL_EXAMPLES[lang]}` : ''}
Keep keys, as is, write translation into values! Here are the strings:

\`\`\`json
${JSON.stringify(strings.reduce((acc, s) => {
                    acc[s.en_string] = '';
                    return acc;
                }, {}), null, 2)}
\`\`\`
`;
                // process.env.HEAVY_DEBUG && console.log('🧠 llm prompt', prompt);
                // call OpenAI
                const resp = yield this.options.completeAdapter.complete(prompt, [], 300);
                // process.env.HEAVY_DEBUG && console.log('🧠 llm resp', resp);
                if (resp.error) {
                    throw new AiTranslateError(resp.error);
                }
                // parse response like
                // Here are the translations for the strings you provided:
                // ```json
                // [{"live": "canlı"}, {"Table Games": "Masa Oyunları"}]
                // ```
                let res;
                try {
                    res = resp.content.split("```json")[1].split("```")[0];
                }
                catch (e) {
                    console.error('error in parsing OpenAI', resp);
                    throw new AiTranslateError('Error in parsing OpenAI response');
                }
                res = JSON.parse(res);
                for (const [enStr, translatedStr] of Object.entries(res)) {
                    const translationsTargeted = translations.filter(t => t[this.enFieldName] === enStr);
                    // might be several with same en_string
                    for (const translation of translationsTargeted) {
                        //translation[this.trFieldNames[lang]] = translatedStr;
                        // process.env.HEAVY_DEBUG && console.log(`🪲translated to ${lang} ${translation.en_string}, ${translatedStr}`)
                        if (!updateStrings[translation[this.primaryKeyFieldName]]) {
                            updateStrings[translation[this.primaryKeyFieldName]] = {
                                updates: {},
                                translatedStr,
                                category: translation[this.options.categoryFieldName],
                                strId: translation[this.primaryKeyFieldName],
                            };
                        }
                        updateStrings[translation[this.primaryKeyFieldName]].updates[this.trFieldNames[lang]] = translatedStr;
                    }
                }
                return Object.keys(updateStrings);
            });
            const langsInvolved = new Set(Object.keys(needToTranslateByLang));
            let totalTranslated = [];
            yield Promise.all(Object.entries(needToTranslateByLang).map((_b) => __awaiter(this, [_b], void 0, function* ([lang, strings]) {
                // first translate without plurals
                const stringsWithoutPlurals = strings.filter(s => !s.en_string.includes('|'));
                process.env.HEAVY_DEBUG && console.log(`🔗  ${lang} noplurals started ${stringsWithoutPlurals.length}`);
                const noPluralKeys = yield translateToLang(lang, stringsWithoutPlurals, false);
                process.env.HEAVY_DEBUG && console.log(`🔗  ${lang} noplurals finished`);
                const stringsWithPlurals = strings.filter(s => s.en_string.includes('|'));
                process.env.HEAVY_DEBUG && console.log(`🔗  ${lang} plurals started ${stringsWithPlurals.length}`);
                const pluralKeys = yield translateToLang(lang, stringsWithPlurals, true);
                process.env.HEAVY_DEBUG && console.log(`🔗  ${lang} plurals finished`);
                totalTranslated = totalTranslated.concat(noPluralKeys, pluralKeys);
            })));
            process.env.HEAVY_DEBUG && console.log('updateStrings were formed', totalTranslated);
            yield Promise.all(Object.entries(updateStrings).map((_c) => __awaiter(this, [_c], void 0, function* ([_, { updates, strId }]) {
                // because this will translate all languages, we can set completedLangs to all languages
                const futureCompletedFieldValue = this.fullCompleatedFieldValue;
                yield this.adminforth.resource(this.resourceConfig.resourceId).update(strId, Object.assign(Object.assign({}, updates), { [this.options.completedFieldName]: futureCompletedFieldValue }));
            })));
            for (const lang of langsInvolved) {
                yield this.cache.clear(`${this.resourceConfig.resourceId}:frontend:${lang}`);
                for (const [enStr, { category }] of Object.entries(updateStrings)) {
                    yield this.cache.clear(`${this.resourceConfig.resourceId}:${category}:${lang}:${enStr}`);
                }
            }
            return new Set(totalTranslated).size;
        });
    }
    processExtractedMessages(adminforth, filePath) {
        return __awaiter(this, void 0, void 0, function* () {
            yield processFrontendMessagesQueue.wait();
            // messages file is in i18n-messages.json
            let messages;
            try {
                messages = yield fs.readJson(filePath);
                process.env.HEAVY_DEBUG && console.info('🐛 Messages file found');
            }
            catch (e) {
                process.env.HEAVY_DEBUG && console.error('🐛 Messages file not yet exists, probably npm run i18n:extract not finished/started yet, might be ok');
                return;
            }
            // loop over missingKeys[i].path and add them to database if not exists
            const missingKeysDeduplicated = messages.missingKeys.reduce((acc, missingKey) => {
                if (!acc.find((a) => a.path === missingKey.path)) {
                    acc.push(missingKey);
                }
                return acc;
            }, []);
            yield Promise.all(missingKeysDeduplicated.map((missingKey) => __awaiter(this, void 0, void 0, function* () {
                const key = missingKey.path;
                const file = missingKey.file;
                const category = 'frontend';
                const exists = yield adminforth.resource(this.resourceConfig.resourceId).count(Filters.EQ(this.enFieldName, key));
                if (exists) {
                    return;
                }
                if (!key) {
                    throw new Error(`Faced an empty key in Fronted messages, file ${file}`);
                }
                const record = Object.assign({ [this.enFieldName]: key, [this.options.categoryFieldName]: category }, (this.options.sourceFieldName ? { [this.options.sourceFieldName]: file } : {}));
                try {
                    yield adminforth.resource(this.resourceConfig.resourceId).create(record);
                }
                catch (e) {
                    console.error('🐛 Error creating record', e);
                }
            })));
            // updateBadge
            this.updateUntranslatedMenuBadge();
        });
    }
    tryProcessAndWatch(adminforth) {
        return __awaiter(this, void 0, void 0, function* () {
            const serveDir = adminforth.codeInjector.getServeDir();
            // messages file is in i18n-messages.json
            const messagesFile = path.join(serveDir, 'i18n-messages.json');
            process.env.HEAVY_DEBUG && console.log('🪲🔔messagesFile read started', messagesFile);
            this.processExtractedMessages(adminforth, messagesFile);
            // we use watcher because file can't be yet created when we start - bundleNow can be done in build time or can be done now
            // that is why we make attempt to process it now and then watch for changes
            const w = chokidar.watch(messagesFile, {
                persistent: true,
                ignoreInitial: true, // don't trigger 'add' event for existing file on start
            });
            w.on('change', () => {
                process.env.HEAVY_DEBUG && console.log('🪲🔔messagesFile change', messagesFile);
                this.processExtractedMessages(adminforth, messagesFile);
            });
            w.on('add', () => {
                process.env.HEAVY_DEBUG && console.log('🪲🔔messagesFile add', messagesFile);
                this.processExtractedMessages(adminforth, messagesFile);
            });
        });
    }
    validateConfigAfterDiscover(adminforth, resourceConfig) {
        // optional method where you can safely check field types after database discovery was performed
        // ensure each trFieldName (apart from enFieldName) is nullable column of type string
        for (const lang of this.options.supportedLanguages) {
            if (lang === 'en') {
                continue;
            }
            const column = resourceConfig.columns.find(c => c.name === this.trFieldNames[lang]);
            if (!column) {
                throw new Error(`Field ${this.trFieldNames[lang]} not found for storing translation for language ${lang}
           in resource ${resourceConfig.resourceId}, consider adding it to columns or change trFieldNames option to remap it to existing column`);
            }
            if (column.required.create || column.required.edit) {
                throw new Error(`Field ${this.trFieldNames[lang]} should be not required in resource ${resourceConfig.resourceId}`);
            }
        }
        // ensure categoryFieldName defined and is string
        if (!this.options.categoryFieldName) {
            throw new Error(`categoryFieldName option is not defined. It is used to categorize translations and return only specific category e.g. to frontend`);
        }
        const categoryColumn = resourceConfig.columns.find(c => c.name === this.options.categoryFieldName);
        if (!categoryColumn) {
            throw new Error(`Field ${this.options.categoryFieldName} not found in resource ${resourceConfig.resourceId}`);
        }
        if (categoryColumn.type !== AdminForthDataTypes.STRING && categoryColumn.type !== AdminForthDataTypes.TEXT) {
            throw new Error(`Field ${this.options.categoryFieldName} should be of type string in resource ${resourceConfig.resourceId}, but it is ${categoryColumn.type}`);
        }
        // in this plugin we will use plugin to fill the database with missing language messages
        this.tryProcessAndWatch(adminforth);
        adminforth.tr = (msg, category, lang, params) => __awaiter(this, void 0, void 0, function* () {
            if (!msg) {
                return msg;
            }
            if (category === 'frontend') {
                throw new Error(`Category 'frontend' is reserved for frontend messages, use any other category for backend messages`);
            }
            // console.log('🪲tr', msg, category, lang);
            // if lang is not supported , throw
            if (!this.options.supportedLanguages.includes(lang)) {
                lang = 'en'; // for now simply fallback to english
                // throwing like line below might be too strict, e.g. for custom apis made with fetch which don't pass accept-language
                // throw new Error(`Language ${lang} is not entered to be supported by requested by browser in request headers accept-language`);
            }
            let result;
            // try to get translation from cache
            const cacheKey = `${resourceConfig.resourceId}:${category}:${lang}:${msg}`;
            const cached = yield this.cache.get(cacheKey);
            if (cached) {
                result = cached;
            }
            if (!result) {
                const resource = adminforth.resource(resourceConfig.resourceId);
                const translation = yield resource.get([Filters.EQ(this.enFieldName, msg), Filters.EQ(this.options.categoryFieldName, category)]);
                if (!translation) {
                    yield resource.create({
                        [this.enFieldName]: msg,
                        [this.options.categoryFieldName]: category,
                    });
                    this.updateUntranslatedMenuBadge();
                }
                // do this check here, to faster register missing translations
                // also not cache it - no sense to cache english strings
                if (lang === 'en') {
                    // set to cache to return faster next time
                    result = msg;
                }
                else {
                    result = translation === null || translation === void 0 ? void 0 : translation[this.trFieldNames[lang]];
                    if (!result) {
                        // return english
                        result = msg;
                    }
                }
                // cache so even if key does not exist, we will not hit database
                yield this.cache.set(cacheKey, result);
            }
            if (params) {
                for (const [key, value] of Object.entries(params)) {
                    result = result.replace(`{${key}}`, value);
                }
            }
            return result;
        });
    }
    instanceUniqueRepresentation(pluginOptions) {
        // optional method to return unique string representation of plugin instance. 
        // Needed if plugin can have multiple instances on one resource 
        return `single`;
    }
    setupEndpoints(server) {
        server.endpoint({
            method: 'GET',
            path: `/plugin/${this.pluginInstanceId}/frontend_messages`,
            noAuth: true,
            handler: (_a) => __awaiter(this, [_a], void 0, function* ({ query }) {
                const lang = query.lang;
                // form map of translations
                const resource = this.adminforth.resource(this.resourceConfig.resourceId);
                const cacheKey = `${this.resourceConfig.resourceId}:frontend:${lang}`;
                const cached = yield this.cache.get(cacheKey);
                if (cached) {
                    return cached;
                }
                const translations = {};
                const allTranslations = yield resource.list([Filters.EQ(this.options.categoryFieldName, 'frontend')]);
                for (const tr of allTranslations) {
                    translations[tr[this.enFieldName]] = tr[this.trFieldNames[lang]];
                }
                yield this.cache.set(cacheKey, translations);
                return translations;
            })
        });
    }
}
