v1.0.0
This commit is contained in:
1
.gitattributes
vendored
Normal file
1
.gitattributes
vendored
Normal file
@@ -0,0 +1 @@
|
||||
packs/** binary
|
||||
5
.gitignore
vendored
Normal file
5
.gitignore
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
.git/
|
||||
node_modules/
|
||||
Dockerfile
|
||||
docker-compose.yaml
|
||||
.dockerignore
|
||||
1
.husky/pre-commit
Normal file
1
.husky/pre-commit
Normal file
@@ -0,0 +1 @@
|
||||
npm run lint:fix
|
||||
4
.prettierrc.json
Normal file
4
.prettierrc.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"printWidth": 120,
|
||||
"tabWidth": 2
|
||||
}
|
||||
26
CHANGELOG.md
Normal file
26
CHANGELOG.md
Normal file
@@ -0,0 +1,26 @@
|
||||
# 1.2.1
|
||||
- added support for custom file prompt
|
||||
|
||||
# 1.2.0
|
||||
- added support for different API endpoints
|
||||
|
||||
# 1.1.1
|
||||
- added support journal for 5e
|
||||
- added dropdown selecting GPT model
|
||||
|
||||
# 1.1.0
|
||||
- added support for 5E items
|
||||
- added dropdown for system selection
|
||||
|
||||
# 1.0.7
|
||||
- fixed problem with last foundry version 13.346
|
||||
|
||||
# 1.0.6
|
||||
- Adding Github action to push release
|
||||
|
||||
# 1.0.5
|
||||
- First working release
|
||||
- Removed debug
|
||||
|
||||
# 1.0.1
|
||||
- Kick off
|
||||
55
README.md
Normal file
55
README.md
Normal file
@@ -0,0 +1,55 @@
|
||||
# FoundryVTT Translate ALL
|
||||
|
||||

|
||||

|
||||
|
||||
A small module for FoundryVTT that allows you to translate:
|
||||
- Spells
|
||||
- Items
|
||||
- Abilities
|
||||
- Journal Entries
|
||||
|
||||
into your specified language.
|
||||
|
||||
---
|
||||
|
||||
## Setup Instructions
|
||||
|
||||
1. Visit [https://platform.openai.com/](https://platform.openai.com/)
|
||||
2. Get your API key: [https://platform.openai.com/api-keys](https://platform.openai.com/api-keys)
|
||||
- ⚠️ **Important:** The key will only be shown once. Copy it and store it somewhere safe.
|
||||
- Set a **spending limit**. I'm using `o4-mini`, which is very affordable. However, set a budget—I'm not responsible for any charges.
|
||||
- You’ll get a free trial with some usage credits.
|
||||
- Costs are generally low and depend on how many words you translate. Check [OpenAI pricing](https://openai.com/pricing).
|
||||
3. Enter the API key in the FoundryVTT module settings.
|
||||
4. You're ready to Translate ALL!
|
||||
|
||||
---
|
||||
|
||||
## How It Works
|
||||
|
||||
A new **"Translate"** button will appear where translation is supported:
|
||||

|
||||
|
||||
After clicking it, wait a few seconds and the content will be automatically translated:
|
||||

|
||||
|
||||
---
|
||||
|
||||
## Options
|
||||
|
||||
- if you would like to use the default prompt leave empty the selection of the prompt file
|
||||
|
||||
---
|
||||
|
||||
## Changelog
|
||||
|
||||
View the full changelog [HERE](./CHANGELOG.md)
|
||||
|
||||
---
|
||||
|
||||
## Contributions
|
||||
|
||||
Contributions are welcome! Feature development will be slow and based on community interest. For personal use, the module is already sufficient.
|
||||
|
||||
You can find the current to-do list [HERE](./TODO.md). Tasks are not listed in order of priority.
|
||||
10
TODO.md
Normal file
10
TODO.md
Normal file
@@ -0,0 +1,10 @@
|
||||
- Fix Types
|
||||
- Add spinner while translating and prevent user the interaction
|
||||
- better button (use CSS and module style)
|
||||
- caching same chatgpt request locally
|
||||
- journal need to be manually closed (Pf2e)
|
||||
- transform CRLF to LF on pre-commit
|
||||
- 5ed journal not working (markdowns type)
|
||||
|
||||
Possible additional feature:
|
||||
- text to speech
|
||||
91
eslint.config.mjs
Normal file
91
eslint.config.mjs
Normal file
@@ -0,0 +1,91 @@
|
||||
import { defineConfig, globalIgnores } from "eslint/config";
|
||||
import { fixupConfigRules, fixupPluginRules } from "@eslint/compat";
|
||||
import prettier from "eslint-plugin-prettier";
|
||||
import typescriptEslint from "@typescript-eslint/eslint-plugin";
|
||||
import _import from "eslint-plugin-import";
|
||||
import globals from "globals";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import js from "@eslint/js";
|
||||
import { FlatCompat } from "@eslint/eslintrc";
|
||||
import tsParser from "@typescript-eslint/parser";
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
const compat = new FlatCompat({
|
||||
baseDirectory: __dirname,
|
||||
recommendedConfig: js.configs.recommended,
|
||||
allConfig: js.configs.all
|
||||
});
|
||||
|
||||
export default defineConfig([globalIgnores([
|
||||
"node_modules/",
|
||||
"**/node_modules/",
|
||||
"**/script/",
|
||||
"**/eslint.config.mjs",
|
||||
"**/.eslintrc.js",
|
||||
"**/types/",
|
||||
]), {
|
||||
extends: fixupConfigRules(compat.extends(
|
||||
"eslint:recommended",
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
"plugin:import/errors",
|
||||
"plugin:import/typescript",
|
||||
"prettier",
|
||||
)),
|
||||
|
||||
plugins: {
|
||||
prettier,
|
||||
"@typescript-eslint": fixupPluginRules(typescriptEslint),
|
||||
import: fixupPluginRules(_import),
|
||||
},
|
||||
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.browser,
|
||||
},
|
||||
|
||||
parser: tsParser,
|
||||
ecmaVersion: 2023,
|
||||
sourceType: "module",
|
||||
|
||||
parserOptions: {
|
||||
project: "./tsconfig.json",
|
||||
},
|
||||
},
|
||||
|
||||
settings: {
|
||||
"import/resolver": {
|
||||
node: {
|
||||
paths: ["src", "types", "", "dist"],
|
||||
extensions: [".css", ".js", ".json", ".jsx", ".scss", ".ts", ".tsx"],
|
||||
},
|
||||
|
||||
"eslint-import-resolver-typescript": true,
|
||||
typescript: true,
|
||||
},
|
||||
|
||||
"import/parsers": {
|
||||
"@typescript-eslint/parser": [".ts"],
|
||||
},
|
||||
},
|
||||
|
||||
rules: {
|
||||
eqeqeq: ["error", "always"],
|
||||
"import/no-default-export": "error",
|
||||
"@typescript-eslint/no-explicit-any": "off",
|
||||
'prettier/prettier': [
|
||||
'error',
|
||||
{
|
||||
'endOfLine': 'auto',
|
||||
}
|
||||
],
|
||||
|
||||
"no-console": "error",
|
||||
|
||||
"spaced-comment": ["error", "always", {
|
||||
markers: ["/"],
|
||||
}],
|
||||
},
|
||||
}]);
|
||||
14
lang/en.json
Normal file
14
lang/en.json
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"translate-all.settings.apiKey.name": "OpenAI API Key",
|
||||
"translate-all.settings.apiKey.hint": "Your secret API key to access ChatGPT.",
|
||||
"translate-all.settings.apiEndpoint.name": "API Endpoint",
|
||||
"translate-all.settings.apiEndpoint.hint": "The endpoint to use for the API. Change this if you are using a proxy or a different compatible API.",
|
||||
"translate-all.settings.language.name": "Language",
|
||||
"translate-all.settings.language.hint": "The language to translate descriptions into.",
|
||||
"translate-all.settings.game.system.name": "Select Game System",
|
||||
"translate-all.settings.game.system.hint": "Choose the game system for which you want to translate descriptions.",
|
||||
"translate-all.settings.model.name": "Target Model",
|
||||
"translate-all.settings.model.hint": "The model to use for translation. Default is 'GPT-4o Mini'.",
|
||||
"translate-all.settings.promptTemplatePath.name": "Prompt Template File",
|
||||
"translate-all.settings.promptTemplatePath.hint": "Path to a text file containing a custom prompt (e.g., /modules/translate-all/prompts/dnd5e.txt)"
|
||||
}
|
||||
29
module.json
Normal file
29
module.json
Normal file
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"id": "translate-all",
|
||||
"title": "Translate All",
|
||||
"description": "Adds a button to translate item/spell and Journal descriptions using ChatGPT.",
|
||||
"version": "1.0.0",
|
||||
"type": "module",
|
||||
"compatibility": {
|
||||
"minimum": "13",
|
||||
"maximum": "13",
|
||||
"verified": "13"
|
||||
},
|
||||
"authors": [
|
||||
{
|
||||
"name": "https://gitea.local.lab/Lbenedar",
|
||||
"discord": "Lbenedar"
|
||||
}
|
||||
],
|
||||
"url": "https://gitea.local.lab/Lbenedar/translate-all",
|
||||
"esmodules": [
|
||||
"scripts/main.js"
|
||||
],
|
||||
"languages": [
|
||||
{
|
||||
"lang": "en",
|
||||
"name": "English",
|
||||
"path": "lang/en.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
5406
package-lock.json
generated
Normal file
5406
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
34
package.json
Normal file
34
package.json
Normal file
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"name": "translate-all",
|
||||
"version": "1.2.1",
|
||||
"description": "Foundry VTT module to translate item and spell descriptions using ChatGPT.",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"lint": "eslint ./src --ext .ts",
|
||||
"lint:fix": "eslint ./src --ext .ts --fix",
|
||||
"build": "esbuild src/main.ts --bundle --keep-names --minify --sourcemap --outdir=scripts",
|
||||
"dev": "esbuild src/main.ts --bundle --keep-names --watch --sourcemap --outdir=scripts",
|
||||
"prepare": "husky"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/compat": "^1.2.9",
|
||||
"@eslint/eslintrc": "^3.3.1",
|
||||
"@eslint/js": "^9.27.0",
|
||||
"@league-of-foundry-developers/foundry-vtt-types": "^12.331.5",
|
||||
"@typescript-eslint/eslint-plugin": "^8.33.0",
|
||||
"@typescript-eslint/parser": "^8.34.1",
|
||||
"esbuild": "^0.27.3",
|
||||
"eslint": "^9.27.0",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-import-resolver-typescript": "^3.10.1",
|
||||
"eslint-plugin-import": "^2.31.0",
|
||||
"eslint-plugin-prettier": "^5.4.1",
|
||||
"fvtt-types": "npm:@league-of-foundry-developers/foundry-vtt-types@^12.331.5",
|
||||
"globals": "^16.2.0",
|
||||
"husky": "^9.1.7",
|
||||
"prettier": "^3.5.3",
|
||||
"typescript": "^5.8.3",
|
||||
"typescript-eslint": "^8.32.0"
|
||||
}
|
||||
}
|
||||
2
scripts/main.js
Normal file
2
scripts/main.js
Normal file
File diff suppressed because one or more lines are too long
7
scripts/main.js.map
Normal file
7
scripts/main.js.map
Normal file
File diff suppressed because one or more lines are too long
58
src/handlers/data-handler.ts
Normal file
58
src/handlers/data-handler.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import { Directories, SupportedEntries, SupportedSystems, TranslateFunction } from "types";
|
||||
import { TranslateAllSettingHandler } from "./settings-handler";
|
||||
|
||||
export class DataHandler {
|
||||
static getDescription(app: JournalPageSheet | ItemSheet, type: SupportedEntries) {
|
||||
const system = TranslateAllSettingHandler.getSetting("translate-all", "targetSystem") as SupportedSystems;
|
||||
|
||||
if (type === SupportedEntries.JOURNAL) {
|
||||
return DataHandler.getDescriptionFromJournal(app as JournalPageSheet, system);
|
||||
} else if (type === SupportedEntries.ITEM) {
|
||||
return DataHandler.getDescriptionFromItem(app as ItemSheet, system);
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
static getDescriptionFromJournal(app: JournalPageSheet, system: SupportedSystems): string | undefined {
|
||||
switch (system) {
|
||||
case SupportedSystems.PATHFINDER2E:
|
||||
return (app?.options as any).document.text.content || undefined;
|
||||
case SupportedSystems.DND5E:
|
||||
return (app?.options as any).document.text.content || undefined; // TODO: fix this type casting
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
static getDescriptionFromItem(app: ItemSheet, system: SupportedSystems): string | undefined {
|
||||
switch (system) {
|
||||
case SupportedSystems.PATHFINDER2E:
|
||||
return (app?.object?.system as any).description.value; // TODO: fix this type casting
|
||||
case SupportedSystems.DND5E:
|
||||
return (app?.options as any)?.document.system.description.value || undefined; // TODO: fix this type casting
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
static getPathToUpdate(item: SupportedEntries): string {
|
||||
const system = TranslateAllSettingHandler.getSetting("translate-all", "targetSystem") as SupportedSystems;
|
||||
return Directories[system][item];
|
||||
}
|
||||
|
||||
static async getTranslatedDescription(
|
||||
app: JournalPageSheet | ItemSheet,
|
||||
html: JQuery<HTMLElement>,
|
||||
item: SupportedEntries,
|
||||
translateFN: TranslateFunction,
|
||||
) {
|
||||
const description = DataHandler.getDescription(app, item);
|
||||
if (!description) {
|
||||
// Do not enable button to translate if there is no description
|
||||
return;
|
||||
}
|
||||
const path = DataHandler.getPathToUpdate(item);
|
||||
translateFN(app, html, description, path);
|
||||
}
|
||||
}
|
||||
91
src/handlers/html-handler.ts
Normal file
91
src/handlers/html-handler.ts
Normal file
@@ -0,0 +1,91 @@
|
||||
import { Translator } from "translator";
|
||||
import { SupportedSystems } from "types";
|
||||
import { TranslateAllSettingHandler } from "./settings-handler";
|
||||
|
||||
export class HTMLHandler {
|
||||
static async translateApp(
|
||||
app: JournalPageSheet | ItemSheet,
|
||||
html: JQuery<HTMLElement>,
|
||||
description: string,
|
||||
path: string,
|
||||
): Promise<void> {
|
||||
const htmlQuery: JQuery<HTMLElement> = html instanceof jQuery ? html : $(html);
|
||||
|
||||
const header = htmlQuery.find(".window-header");
|
||||
if (header.find("button.translate-btn").length) return;
|
||||
|
||||
const btn = $(
|
||||
`<button class="translate-btn header-control icon fa-solid fa-earth-americas translate-btn" type="button" data-tooltip="Перевести описание" aria-label="Перевести описание"/>`,
|
||||
);
|
||||
|
||||
btn.on("click", async () => {
|
||||
const translated = await Translator.translate(description);
|
||||
if (!translated) {
|
||||
ui?.notifications?.error("Translation failed or returned empty.");
|
||||
return;
|
||||
}
|
||||
await HTMLHandler.updateDescription(app, translated, path);
|
||||
});
|
||||
const h1Tag = header.find("h1")
|
||||
if (h1Tag) {
|
||||
h1Tag.after(btn);
|
||||
return
|
||||
}
|
||||
header.append(btn);
|
||||
}
|
||||
|
||||
private static async updateDescription(
|
||||
app: JournalPageSheet | ItemSheet,
|
||||
translation: string,
|
||||
path: string,
|
||||
): Promise<void> {
|
||||
const system = TranslateAllSettingHandler.getSetting("translate-all", "targetSystem") as SupportedSystems;
|
||||
if (system === SupportedSystems.DND5E) {
|
||||
await this.update5eDescription(app, translation, path);
|
||||
} else if (system === SupportedSystems.PATHFINDER2E) {
|
||||
await this.updatePF2EDescription(app, translation, path);
|
||||
}
|
||||
}
|
||||
|
||||
private static async update5eDescription(
|
||||
app: JournalPageSheet | ItemSheet,
|
||||
translation: string,
|
||||
path: string,
|
||||
): Promise<void> {
|
||||
try {
|
||||
const item = app.document;
|
||||
await item.update({ [path]: translation });
|
||||
app.render(true);
|
||||
app.close();
|
||||
} catch (error) {
|
||||
ui?.notifications?.error(`Error updating item description: ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
private static async updatePF2EDescription(
|
||||
app: JournalPageSheet | ItemSheet,
|
||||
translation: string,
|
||||
path: string,
|
||||
): Promise<void> {
|
||||
const item = app.object;
|
||||
|
||||
try {
|
||||
if (path.includes("system")) {
|
||||
const item = app.object
|
||||
await item.update({ [path]: translation });
|
||||
} else {
|
||||
const item = app.options.document
|
||||
await item.updateSource({ [path]: translation });
|
||||
}
|
||||
} catch (error) {
|
||||
ui?.notifications?.error(`Error updating item description: ${error}`);
|
||||
}
|
||||
if (path.includes("system")) {
|
||||
app.object.render(true);
|
||||
await app.object.sheet?.close();
|
||||
}
|
||||
|
||||
await app.render(true);
|
||||
await app.close();
|
||||
}
|
||||
}
|
||||
108
src/handlers/settings-handler.ts
Normal file
108
src/handlers/settings-handler.ts
Normal file
@@ -0,0 +1,108 @@
|
||||
import { Translator } from "../translator";
|
||||
import { KeyFor, SupportedSystems, TranslateAllNamespace } from "../types";
|
||||
|
||||
export class TranslateAllSettingHandler {
|
||||
gameSettings: Game["settings"] = game.settings!;
|
||||
readonly settings = {
|
||||
targetSystem: {
|
||||
name: "translate-all.settings.game.system.name",
|
||||
hint: "translate-all.settings.game.system.hint",
|
||||
scope: "world",
|
||||
config: true,
|
||||
type: String,
|
||||
default: SupportedSystems.PATHFINDER2E, // Default to Pathfinder 2e
|
||||
choices: {
|
||||
[SupportedSystems.DND5E]: "D&D 5e",
|
||||
[SupportedSystems.PATHFINDER2E]: "Pathfinder 2e",
|
||||
},
|
||||
},
|
||||
apiEndpoint: {
|
||||
name: "translate-all.settings.apiEndpoint.name",
|
||||
hint: "translate-all.settings.apiEndpoint.hint",
|
||||
scope: "world",
|
||||
config: true,
|
||||
type: String,
|
||||
default: "https://api.openai.com/v1",
|
||||
},
|
||||
targetLanguage: {
|
||||
name: "translate-all.settings.language.name",
|
||||
hint: "translate-all.settings.language.hint",
|
||||
scope: "world",
|
||||
config: true,
|
||||
type: String,
|
||||
default: "Italian", // Default to Italian
|
||||
masked: true,
|
||||
},
|
||||
targetModel: {
|
||||
name: "translate-all.settings.model.name",
|
||||
hint: "translate-all.settings.model.hint",
|
||||
scope: "world",
|
||||
config: true,
|
||||
type: String,
|
||||
default: "gpt-4o-mini",
|
||||
choices: {},
|
||||
},
|
||||
promptModel: {
|
||||
name: "translate-all.settings.promptTemplatePath.name",
|
||||
hint: "translate-all.settings.promptTemplatePath.hint",
|
||||
scope: "world",
|
||||
config: true,
|
||||
type: String,
|
||||
filePicker: true, // 👈 Enables the file picker
|
||||
default: "",
|
||||
},
|
||||
};
|
||||
|
||||
constructor() {}
|
||||
|
||||
async init(): Promise<void> {
|
||||
await this._registerSettings();
|
||||
}
|
||||
|
||||
private async _registerSettings(): Promise<void> {
|
||||
this._register(
|
||||
"translate-all" as TranslateAllNamespace,
|
||||
"targetSystem" as KeyFor<TranslateAllNamespace>,
|
||||
this.settings.targetSystem,
|
||||
);
|
||||
this._register(
|
||||
"translate-all" as TranslateAllNamespace,
|
||||
"apiEndpoint" as KeyFor<TranslateAllNamespace>,
|
||||
this.settings.apiEndpoint,
|
||||
);
|
||||
this._register(
|
||||
"translate-all" as TranslateAllNamespace,
|
||||
"targetLanguage" as KeyFor<TranslateAllNamespace>,
|
||||
this.settings.targetLanguage,
|
||||
);
|
||||
const models = await Translator.getModels();
|
||||
if (models) {
|
||||
this.settings.targetModel.choices = models;
|
||||
}
|
||||
|
||||
this._register(
|
||||
"translate-all" as TranslateAllNamespace,
|
||||
"targetModel" as KeyFor<TranslateAllNamespace>,
|
||||
this.settings.targetModel,
|
||||
);
|
||||
this._register(
|
||||
"translate-all" as TranslateAllNamespace,
|
||||
"promptTemplatePath" as KeyFor<TranslateAllNamespace>,
|
||||
this.settings.promptModel,
|
||||
);
|
||||
}
|
||||
|
||||
// TODO: Fix this type casting
|
||||
_register(namespace: TranslateAllNamespace, key: KeyFor<TranslateAllNamespace>, config: any): void {
|
||||
this.gameSettings.register(namespace as "core", key as KeyFor<"core">, config);
|
||||
}
|
||||
|
||||
// TODO: Fix this type casting
|
||||
static getSetting(
|
||||
namespace: TranslateAllNamespace,
|
||||
key: KeyFor<TranslateAllNamespace>,
|
||||
): string | boolean | number | object | undefined {
|
||||
const gameSettings = game.settings!;
|
||||
return gameSettings.get(namespace as "core", key as KeyFor<"core">);
|
||||
}
|
||||
}
|
||||
29
src/main.ts
Normal file
29
src/main.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import { DataHandler } from "handlers/data-handler";
|
||||
import { HTMLHandler } from "handlers/html-handler";
|
||||
import { TranslateAllSettingHandler } from "handlers/settings-handler";
|
||||
import { SupportedEntries, SupportedSystems } from "types";
|
||||
|
||||
Hooks.once("init", async () => {
|
||||
if (!game.settings) {
|
||||
ui?.notifications?.error(`Game settings are not available. This module requires Foundry VTT version 10 or later.`);
|
||||
return;
|
||||
}
|
||||
const settingHandler = new TranslateAllSettingHandler();
|
||||
await settingHandler.init();
|
||||
});
|
||||
|
||||
Hooks.on("renderJournalPageSheet", async (app: JournalPageSheet, html: JQuery<HTMLElement>) => {
|
||||
DataHandler.getTranslatedDescription(app, html, SupportedEntries.JOURNAL, HTMLHandler.translateApp);
|
||||
});
|
||||
|
||||
Hooks.on("renderItemSheet", async (app: ItemSheet, html: JQuery<HTMLElement>) => {
|
||||
DataHandler.getTranslatedDescription(app, html, SupportedEntries.ITEM, HTMLHandler.translateApp);
|
||||
});
|
||||
|
||||
Hooks.on("renderItemSheet5e", async (app: ItemSheet, html: JQuery<HTMLElement>) => {
|
||||
DataHandler.getTranslatedDescription(app, html, SupportedEntries.ITEM, HTMLHandler.translateApp);
|
||||
});
|
||||
|
||||
Hooks.on("renderJournalEntryPageSheet", async (app: JournalPageSheet, html: JQuery<HTMLElement>) => {
|
||||
DataHandler.getTranslatedDescription(app, html, SupportedEntries.JOURNAL, HTMLHandler.translateApp);
|
||||
});
|
||||
112
src/translator.ts
Normal file
112
src/translator.ts
Normal file
@@ -0,0 +1,112 @@
|
||||
import { TranslateAllSettingHandler } from "handlers/settings-handler";
|
||||
import { SupportedLanguages, SupportedSystems } from "types";
|
||||
|
||||
export class Translator {
|
||||
static async translate(description: string): Promise<string | undefined> {
|
||||
return await Translator.translateWithChatLlama(description);
|
||||
}
|
||||
|
||||
static async getPromptTemplate(path: string, description: string): Promise<string> {
|
||||
const promptTemplatePath = TranslateAllSettingHandler.getSetting("translate-all", "promptTemplatePath") as string;
|
||||
if (!promptTemplatePath) {
|
||||
return "";
|
||||
}
|
||||
let promptTemplate = "";
|
||||
if (promptTemplatePath) {
|
||||
try {
|
||||
const url = foundry.utils.getRoute(promptTemplatePath);
|
||||
promptTemplate = await fetch(url).then((x) => x.text());
|
||||
} catch (err) {
|
||||
ui?.notifications?.warn(`Could not load prompt template. ${err}`);
|
||||
}
|
||||
}
|
||||
|
||||
return promptTemplate + `: ${description}`;
|
||||
}
|
||||
|
||||
static async generatePrompt(
|
||||
system: SupportedSystems,
|
||||
language: SupportedLanguages,
|
||||
description: string,
|
||||
): Promise<string> {
|
||||
const path = TranslateAllSettingHandler.getSetting("translate-all", "promptTemplatePath") as string;
|
||||
let prompt = "";
|
||||
if (path) {
|
||||
prompt = await Translator.getPromptTemplate(path, description);
|
||||
} else {
|
||||
prompt = `Translate the following ${system} description into ${language}:\
|
||||
You recieve data formatted in HTML. Don't change format and structure, like HTML tags. Don't add new HTML tags and don't change the nesting of HTML tags.\
|
||||
If quotation marks are used, they must be closed. Don't add extra quotation marks.\
|
||||
Do not add any additional code encapsulation or formatting. Just return the translated text.\
|
||||
${description}.`;
|
||||
}
|
||||
|
||||
return prompt;
|
||||
}
|
||||
|
||||
static async getModels(): Promise<Record<string, string> | undefined> {
|
||||
let response;
|
||||
const apiEndpoint = TranslateAllSettingHandler.getSetting("translate-all", "apiEndpoint");
|
||||
|
||||
try {
|
||||
response = await fetch(`${apiEndpoint}/api/tags`, {
|
||||
method: "GET",
|
||||
});
|
||||
} catch (error) {
|
||||
ui?.notifications?.error(`Ollama API call failed. ${error}`);
|
||||
}
|
||||
|
||||
if (!response?.ok) {
|
||||
ui?.notifications?.error("Ollama API call failed.");
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
const models = data.models.reduce((acc: Record<string, string>, model: { model: string }) => {
|
||||
acc[model.model] = model.model;
|
||||
return acc;
|
||||
}, {});
|
||||
return models;
|
||||
}
|
||||
|
||||
static async translateWithChatLlama(description: string): Promise<string | undefined> {
|
||||
let response;
|
||||
const apiEndpoint = TranslateAllSettingHandler.getSetting("translate-all", "apiEndpoint");
|
||||
const system = TranslateAllSettingHandler.getSetting("translate-all", "targetSystem") as SupportedSystems;
|
||||
const language = TranslateAllSettingHandler.getSetting("translate-all", "targetLanguage") as SupportedLanguages;
|
||||
const model = TranslateAllSettingHandler.getSetting("translate-all", "targetModel");
|
||||
const prompt = await Translator.generatePrompt(system, language, description);
|
||||
|
||||
try {
|
||||
const body = JSON.stringify({
|
||||
model: model,
|
||||
prompt: prompt,
|
||||
stream: false,
|
||||
})
|
||||
console.log(`Body. ${body}`);
|
||||
response = await fetch(`${apiEndpoint}/generate`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: body,
|
||||
});
|
||||
} catch (error) {
|
||||
ui?.notifications?.error(`Ollama API call failed. ${error}`);
|
||||
}
|
||||
|
||||
if (!response?.ok) {
|
||||
ui?.notifications?.error(`Ollama API call failed. ${response?.status}`);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
let responseData = data?.response
|
||||
if (responseData.startsWith("```html")) {
|
||||
// Get the part of the string starting from the index immediately after the prefix
|
||||
responseData = responseData.substring("\`\`\`html\n".length, responseData.length - "\n\`\`\`".length);
|
||||
}
|
||||
console.log(`Response after cut: ${responseData}`);
|
||||
return responseData ?? undefined;
|
||||
}
|
||||
}
|
||||
57
src/types/index.ts
Normal file
57
src/types/index.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
export const MODULE_NAME = 'translate-all';
|
||||
|
||||
export interface TranslateConfigSettingConfig {
|
||||
'translate-all.apiKey': string;
|
||||
'translate-all.targetSystem': string;
|
||||
'translate-all.targetLanguage': string;
|
||||
'translate-all.targetModel': string;
|
||||
'translate-all.apiEndpoint': string;
|
||||
'translate-all.promptTemplatePath': string;
|
||||
}
|
||||
|
||||
export type TranslateAllNamespace = typeof MODULE_NAME | ClientSettings.Namespace;
|
||||
|
||||
type GetKeys<
|
||||
N extends string,
|
||||
SettingPath extends PropertyKey,
|
||||
> = SettingPath extends `${N}.${infer Name}` ? Name : never;
|
||||
export type KeyFor<N extends TranslateAllNamespace> = GetKeys<
|
||||
N,
|
||||
keyof TranslateConfigSettingConfig
|
||||
>;
|
||||
|
||||
export interface TranslateFunction {
|
||||
(
|
||||
app: JournalPageSheet | ItemSheet,
|
||||
html: JQuery<HTMLElement>,
|
||||
description: string,
|
||||
path: string,
|
||||
): Promise<void>;
|
||||
}
|
||||
|
||||
export enum SupportedSystems {
|
||||
DND5E = 'D&D5E',
|
||||
PATHFINDER2E = 'PF2E',
|
||||
}
|
||||
|
||||
export enum SupportedLanguages {
|
||||
ENGLISH = 'english',
|
||||
ITALIAN = 'italian',
|
||||
RUSSIAN = 'russian',
|
||||
}
|
||||
|
||||
export enum SupportedEntries {
|
||||
JOURNAL = 'journal',
|
||||
ITEM = 'item',
|
||||
}
|
||||
|
||||
export const Directories = {
|
||||
[SupportedSystems.DND5E]: {
|
||||
[SupportedEntries.JOURNAL]: 'text.content',
|
||||
[SupportedEntries.ITEM]: 'system.description.value',
|
||||
},
|
||||
[SupportedSystems.PATHFINDER2E]: {
|
||||
[SupportedEntries.JOURNAL]: 'text.content',
|
||||
[SupportedEntries.ITEM]: 'system.description.value',
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user