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