From 4d0de8419ca3e3290fca068d1d7f28402ab318a1 Mon Sep 17 00:00:00 2001 From: Alamion Date: Mon, 23 Feb 2026 21:42:25 +0300 Subject: [PATCH 1/7] fixes: - now stats, attributes, characters stats have a changeable id - now all additional promts are stacked in 2 lines --- .gitignore | 4 ++-- manifest.json | 2 +- src/systems/ui/trackerEditor.js | 29 +++++++++++++++++++++++++---- src/utils/transformations.js | 11 +++++++++++ style.css | 15 +++++++++------ 5 files changed, 48 insertions(+), 13 deletions(-) create mode 100644 src/utils/transformations.js diff --git a/.gitignore b/.gitignore index 789b14f..1e0df72 100644 --- a/.gitignore +++ b/.gitignore @@ -22,5 +22,5 @@ node_modules/ # Environment variables .env -# Claude -CLAUDE.md \ No newline at end of file +# Claude +CLAUDE.md diff --git a/manifest.json b/manifest.json index ff16a2a..55c1c6d 100644 --- a/manifest.json +++ b/manifest.json @@ -6,6 +6,6 @@ "js": "index.js", "css": "style.css", "author": "Marinara", - "version": "3.7.2", + "version": "3.7.3", "homePage": "https://github.com/SpicyMarinara/rpg-companion-sillytavern" } diff --git a/src/systems/ui/trackerEditor.js b/src/systems/ui/trackerEditor.js index 90718ff..7a0cabf 100644 --- a/src/systems/ui/trackerEditor.js +++ b/src/systems/ui/trackerEditor.js @@ -31,6 +31,7 @@ import { renderUserStats } from '../rendering/userStats.js'; import { renderInfoBox } from '../rendering/infoBox.js'; import { renderThoughts } from '../rendering/thoughts.js'; import { updateFabWidgets } from './mobile.js'; +import { safeToSnake } from '../../utils/transformations.js'; let $editorModal = null; let activeTab = 'userStats'; @@ -38,6 +39,18 @@ let tempConfig = null; // Temporary config for cancel functionality let tempAssociation = null; // Temporary association state: { presetId: string|null, entityKey: string|null } let originalAssociation = null; // Original association when editor opened + +function set_ids_names(list_with_stats, index, value) { + list_with_stats[index].name = value; + const ids = list_with_stats.toSpliced(index, 1).map(stat => stat.id); + const snake_value = safeToSnake(value); // new id format + if (snake_value !== value && !ids.includes(snake_value)) { // check if this id already exists + list_with_stats[index].id = snake_value; + } + return list_with_stats; +} + + /** * Initialize the tracker editor modal */ @@ -885,7 +898,9 @@ function setupUserStatsListeners() { // Rename stat $('.rpg-stat-name').off('blur').on('blur', function () { const index = $(this).data('index'); - extensionSettings.trackerConfig.userStats.customStats[index].name = $(this).val(); + const value = $(this).val(); + const list_with_stats = extensionSettings.trackerConfig.userStats.customStats + set_ids_names(list_with_stats, index, value); }); // Change stat max value @@ -943,7 +958,9 @@ function setupUserStatsListeners() { // Rename attribute $('.rpg-attr-name').off('blur').on('blur', function () { const index = $(this).data('index'); - extensionSettings.trackerConfig.userStats.rpgAttributes[index].name = $(this).val(); + const value = $(this).val(); + const list_with_stats = extensionSettings.trackerConfig.userStats.rpgAttributes + set_ids_names(list_with_stats, index, value); }); // Enable/disable RPG Attributes section toggle @@ -1394,7 +1411,9 @@ function setupPresentCharactersListeners() { // Rename field $('.rpg-field-label').off('blur').on('blur', function () { const index = $(this).data('index'); - extensionSettings.trackerConfig.presentCharacters.customFields[index].name = $(this).val(); + const value = $(this).val(); + const list_with_stats = extensionSettings.trackerConfig.presentCharacters.customFields + set_ids_names(list_with_stats, index, value); }); // Update description @@ -1443,7 +1462,9 @@ function setupPresentCharactersListeners() { // Rename character stat $('.rpg-char-stat-label').off('blur').on('blur', function () { const index = $(this).data('index'); - extensionSettings.trackerConfig.presentCharacters.characterStats.customStats[index].name = $(this).val(); + const value = $(this).val(); + const list_with_stats = extensionSettings.trackerConfig.presentCharacters.characterStats.customStats + set_ids_names(list_with_stats, index, value); }); } diff --git a/src/utils/transformations.js b/src/utils/transformations.js new file mode 100644 index 0000000..6778ebe --- /dev/null +++ b/src/utils/transformations.js @@ -0,0 +1,11 @@ +const toSnake = str => str + .replace(/[^a-zA-Z]/g, '_') + .replace(/([A-Z])/g, '_$1') + .toLowerCase() + .replace(/_+/g, '_') + .replace(/^_|_$/g, ''); + +export const safeToSnake = (str) => { + const res = toSnake(str); + return (res.length >= 2) ? res : str; // considering element with one symbol is too short to be safe +}; diff --git a/style.css b/style.css index 68b01ef..c1b22c1 100644 --- a/style.css +++ b/style.css @@ -10732,7 +10732,10 @@ body[data-theme="cyberpunk"] .rpg-music-widget-play { /* Features row container */ .rpg-features-row { - display: flex; + display: grid; + grid-template-rows: repeat(2, 1fr); + grid-auto-flow: column; + grid-auto-columns: min-content; gap: 8px; margin-bottom: 12px; overflow-x: auto; @@ -10743,11 +10746,11 @@ body[data-theme="cyberpunk"] .rpg-music-widget-play { } /* Center items when they fit, allow scrolling when they don't */ -.rpg-features-row::before, -.rpg-features-row::after { - content: ''; - margin: auto; -} +/*.rpg-features-row::before,*/ +/*.rpg-features-row::after {*/ +/* content: '';*/ +/* margin: auto;*/ +/*}*/ /* Hide scrollbar for cleaner look while maintaining functionality */ .rpg-features-row::-webkit-scrollbar { From 66a22c74d0a6b0132638e0002d4cc44a5e7c2292 Mon Sep 17 00:00:00 2001 From: Spicy Marinara Date: Wed, 25 Feb 2026 23:58:02 +0100 Subject: [PATCH 2/7] Update src/utils/transformations.js Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/utils/transformations.js | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/utils/transformations.js b/src/utils/transformations.js index 6778ebe..423cb7f 100644 --- a/src/utils/transformations.js +++ b/src/utils/transformations.js @@ -1,9 +1,14 @@ -const toSnake = str => str - .replace(/[^a-zA-Z]/g, '_') - .replace(/([A-Z])/g, '_$1') - .toLowerCase() +const toSnake = (str) => str + // replace any sequence of non-alphanumeric characters with a single underscore + .replace(/[^0-9A-Za-z]+/g, '_') + // insert underscore between a lower-case letter/digit and an upper-case letter (but not between consecutive uppers) + .replace(/([a-z0-9])([A-Z])/g, '$1_$2') + // collapse multiple underscores .replace(/_+/g, '_') - .replace(/^_|_$/g, ''); + // trim leading/trailing underscores + .replace(/^_+|_+$/g, '') + // finally, lowercase the result + .toLowerCase(); export const safeToSnake = (str) => { const res = toSnake(str); From 7305af8f8865fac380689c181ae644e405c9cd85 Mon Sep 17 00:00:00 2001 From: Spicy Marinara Date: Wed, 25 Feb 2026 23:58:26 +0100 Subject: [PATCH 3/7] Update src/systems/ui/trackerEditor.js Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/systems/ui/trackerEditor.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/systems/ui/trackerEditor.js b/src/systems/ui/trackerEditor.js index 7a0cabf..d4f6f48 100644 --- a/src/systems/ui/trackerEditor.js +++ b/src/systems/ui/trackerEditor.js @@ -44,7 +44,8 @@ function set_ids_names(list_with_stats, index, value) { list_with_stats[index].name = value; const ids = list_with_stats.toSpliced(index, 1).map(stat => stat.id); const snake_value = safeToSnake(value); // new id format - if (snake_value !== value && !ids.includes(snake_value)) { // check if this id already exists + const currentId = list_with_stats[index].id; + if (snake_value !== currentId && !ids.includes(snake_value)) { // check if this id already exists list_with_stats[index].id = snake_value; } return list_with_stats; From 131b28fc1fad2e155b240ed3030dbd87d200569e Mon Sep 17 00:00:00 2001 From: Spicy Marinara Date: Wed, 25 Feb 2026 23:58:37 +0100 Subject: [PATCH 4/7] Update src/systems/ui/trackerEditor.js Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/systems/ui/trackerEditor.js | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/src/systems/ui/trackerEditor.js b/src/systems/ui/trackerEditor.js index d4f6f48..4c27709 100644 --- a/src/systems/ui/trackerEditor.js +++ b/src/systems/ui/trackerEditor.js @@ -42,11 +42,28 @@ let originalAssociation = null; // Original association when editor opened function set_ids_names(list_with_stats, index, value) { list_with_stats[index].name = value; + const item = list_with_stats[index]; + const oldId = item?.id; + + item.name = value; const ids = list_with_stats.toSpliced(index, 1).map(stat => stat.id); const snake_value = safeToSnake(value); // new id format - const currentId = list_with_stats[index].id; - if (snake_value !== currentId && !ids.includes(snake_value)) { // check if this id already exists - list_with_stats[index].id = snake_value; + if (snake_value !== value && !ids.includes(snake_value)) { // check if this id already exists + item.id = snake_value; + } + + const newId = item.id; + // If the ID changed, migrate any stored values keyed by the old ID + if (oldId && newId && oldId !== newId) { + if (extensionSettings.userStats && Object.prototype.hasOwnProperty.call(extensionSettings.userStats, oldId)) { + extensionSettings.userStats[newId] = extensionSettings.userStats[oldId]; + delete extensionSettings.userStats[oldId]; + } + + if (extensionSettings.classicStats && Object.prototype.hasOwnProperty.call(extensionSettings.classicStats, oldId)) { + extensionSettings.classicStats[newId] = extensionSettings.classicStats[oldId]; + delete extensionSettings.classicStats[oldId]; + } } return list_with_stats; } From b1098a2721210db76feb5ac1caded223cc38f3fc Mon Sep 17 00:00:00 2001 From: Spicy Marinara Date: Wed, 25 Feb 2026 23:58:44 +0100 Subject: [PATCH 5/7] Update src/systems/ui/trackerEditor.js Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/systems/ui/trackerEditor.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/systems/ui/trackerEditor.js b/src/systems/ui/trackerEditor.js index 4c27709..f287847 100644 --- a/src/systems/ui/trackerEditor.js +++ b/src/systems/ui/trackerEditor.js @@ -46,7 +46,7 @@ function set_ids_names(list_with_stats, index, value) { const oldId = item?.id; item.name = value; - const ids = list_with_stats.toSpliced(index, 1).map(stat => stat.id); + const ids = list_with_stats.filter((_, i) => i !== index).map(stat => stat.id); const snake_value = safeToSnake(value); // new id format if (snake_value !== value && !ids.includes(snake_value)) { // check if this id already exists item.id = snake_value; From 75c8f9b63af2c54230ce27d9fbc4fa4e2bc3d39f Mon Sep 17 00:00:00 2001 From: Spicy Marinara Date: Wed, 25 Feb 2026 23:58:51 +0100 Subject: [PATCH 6/7] Update src/systems/ui/trackerEditor.js Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/systems/ui/trackerEditor.js | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/systems/ui/trackerEditor.js b/src/systems/ui/trackerEditor.js index f287847..e84c75b 100644 --- a/src/systems/ui/trackerEditor.js +++ b/src/systems/ui/trackerEditor.js @@ -58,14 +58,14 @@ function set_ids_names(list_with_stats, index, value) { if (extensionSettings.userStats && Object.prototype.hasOwnProperty.call(extensionSettings.userStats, oldId)) { extensionSettings.userStats[newId] = extensionSettings.userStats[oldId]; delete extensionSettings.userStats[oldId]; - } - - if (extensionSettings.classicStats && Object.prototype.hasOwnProperty.call(extensionSettings.classicStats, oldId)) { - extensionSettings.classicStats[newId] = extensionSettings.classicStats[oldId]; - delete extensionSettings.classicStats[oldId]; - } +function setIdsNames(listWithStats, index, value) { + listWithStats[index].name = value; + const ids = listWithStats.toSpliced(index, 1).map(stat => stat.id); + const snakeValue = safeToSnake(value); // new id format + if (snakeValue !== value && !ids.includes(snakeValue)) { // check if this id already exists + listWithStats[index].id = snakeValue; } - return list_with_stats; + return listWithStats; } From 933d78e192bce1dbd6cced48342619d22272c57e Mon Sep 17 00:00:00 2001 From: Alamion Date: Mon, 2 Mar 2026 20:54:15 +0300 Subject: [PATCH 7/7] feat: localization validator for missing internationalization fix: trackerEditor.js doesn't have syntax issues anymore. --- .gitignore | 1 + package-lock.json | 6 + package.json | 19 +++ src/i18n/validator.js | 264 +++++++++++++++++++++++++++++ src/systems/rendering/userStats.js | 2 +- src/systems/ui/trackerEditor.js | 14 +- 6 files changed, 298 insertions(+), 8 deletions(-) create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 src/i18n/validator.js diff --git a/.gitignore b/.gitignore index 1e0df72..e7bb1e1 100644 --- a/.gitignore +++ b/.gitignore @@ -24,3 +24,4 @@ node_modules/ # Claude CLAUDE.md +yarn.lock diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..9dcafce --- /dev/null +++ b/package-lock.json @@ -0,0 +1,6 @@ +{ + "name": "rpg-companion-sillytavern", + "lockfileVersion": 3, + "requires": true, + "packages": {} +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..b290249 --- /dev/null +++ b/package.json @@ -0,0 +1,19 @@ +{ + "name": "rpg-complanion-sillytavern", + "version": "3.7.3", + "description": "", + "main": "index.js", + "scripts": { + "validate_locale": "node src/i18n/validator.js --watch", + "validate_locale_once": "node src/i18n/validator.js" + }, + "keywords": [], + "author": "", + "license": "MIT", + "devDependencies": { + "chokidar": "^5.0.0", + "fs-extra": "^11.3.3", + "glob": "^13.0.6" + }, + "dependencies": {} +} diff --git a/src/i18n/validator.js b/src/i18n/validator.js new file mode 100644 index 0000000..47dcace --- /dev/null +++ b/src/i18n/validator.js @@ -0,0 +1,264 @@ +const fs = require('fs-extra'); +const path = require('path'); +const chokidar = require('chokidar'); +const glob = require('glob'); + +const COMPILED_DIR = __dirname // path.join(__dirname, 'compiled'); + +function findUnlocalizedText() { + const srcArg = process.argv.find(arg => arg.startsWith('--src=')); + const srcDir = srcArg ? srcArg.split('=')[1] : '.'; + + console.log(`\n๐Ÿ”Ž Scanning for unlocalized text in ${srcDir}...`); + + const files = glob.sync(`${srcDir}/**/*.{html,js,jsx}`, { + ignore: ['**/node_modules/**', '**/dist/**', '**/build/**'] + }); + + if (files.length === 0) { + console.log('โš ๏ธ No .html/.js/.jsx files found'); + return; + } + + let totalFound = 0; + + for (const file of files) { + const content = fs.readFileSync(file, 'utf8'); + const lines = content.split('\n'); + const relPath = path.relative(process.cwd(), file); + + // Searching for string number + lines.forEach((line, index) => { + let match; + const localPattern = /<([a-zA-Z][a-zA-Z0-9]*)(?:\s(?:[^>](?!data-i18n-key))*)?>([\p{L}\p{N}\s\-.,!?:'"()]+)<\/\1>/gu; + + while ((match = localPattern.exec(line)) !== null) { + const text = match[2].trim(); + if (!text) continue; + + // Passing JSX expressions like {someVar} + if (text.includes('{') || text.includes('}')) continue; + + // Passing if tag has data-i18n-key + if (match[0].includes('data-i18n-key')) continue; + + console.log(` - ${relPath}:${index + 1} โ€” <${match[1]}> "${text}"`); + totalFound++; + } + }); + } + + if (totalFound === 0) { + console.log('โœ… No unlocalized text found!'); + } else { + console.log(`\n๐Ÿ“‹ Found ${totalFound} potentially unlocalized text node(s)`); + } +} + + +// Function to validate translations +function validateTranslations() { + console.log('๐Ÿ” Validating translation files...'); + + // Parse --locales=en,fr argument + const localesArg = process.argv.find(arg => arg.startsWith('--locales=')); + const selectedLocales = localesArg + ? localesArg.split('=')[1].split(',').map(l => l.trim()) + : null; + + const files = fs.readdirSync(COMPILED_DIR) + .filter(file => file.endsWith('.json')) + .filter(file => { + const locale = path.basename(file, '.json'); + return !selectedLocales || selectedLocales.includes(locale); + }); + + if (files.length === 0) { + console.log('โš ๏ธ No compiled translation files found'); + return; + } + + // Load all translation data + const translations = {}; + for (const file of files) { + const locale = path.basename(file, '.json'); + const filePath = path.join(COMPILED_DIR, file); + translations[locale] = fs.readJsonSync(filePath); + } + + // Get all locales + const locales = Object.keys(translations); + console.log(`๐Ÿ“ Found ${locales.length} locales: ${locales.join(', ')}`); + + if (locales.length < 2) { + console.log('โš ๏ธ Need at least 2 locales to compare'); + return; + } + + // Choose the first locale as reference + const referenceLocale = locales[0]; + console.log(`๐Ÿ”‘ Using ${referenceLocale} as reference locale`); + + // Get all keys from reference locale + const referenceKeys = Object.keys(translations[referenceLocale]); + console.log(`๐Ÿ”ข Reference locale has ${referenceKeys.size} unique keys`); + + // Track statistics + const stats = { + missingKeys: {}, + extraKeys: {}, + typeErrors: {} + }; + + // Initialize stats for each locale + for (const locale of locales) { + if (locale !== referenceLocale) { + stats.missingKeys[locale] = []; + stats.extraKeys[locale] = []; + stats.typeErrors[locale] = []; + } + } + + // Check each locale against the reference + for (const locale of locales) { + if (locale === referenceLocale) continue; + + const localeKeys = Object.keys(translations[locale]); + + // Check for missing keys + for (const key of referenceKeys) { + if (!key in translations[locale]) { + stats.missingKeys[locale].push(key); + } else { + // Check for type mismatches + const refValue = translations[referenceLocale][key]; + const localeValue = translations[locale][key]; + + if (typeof refValue !== typeof localeValue) { + stats.typeErrors[locale].push({ + key, + refType: typeof refValue, + localeType: typeof localeValue + }); + } + } + } + + // Check for extra keys + for (const key of localeKeys) { + if (!key in translations[referenceLocale]) { + stats.extraKeys[locale].push(key); + } + } + } + + // Print results + let hasIssues = false; + + // Print missing keys + for (const locale in stats.missingKeys) { + const missing = stats.missingKeys[locale]; + if (missing.length > 0) { + hasIssues = true; + console.log(`โŒ ${locale} is missing ${missing.length} keys:`); + missing.forEach(key => { + console.log(` - ${key}`); + }); + } + } + + // Print extra keys + for (const locale in stats.extraKeys) { + const extra = stats.extraKeys[locale]; + if (extra.length > 0) { + hasIssues = true; + console.log(`โš ๏ธ ${locale} has ${extra.length} extra keys:`); + extra.forEach(key => { + console.log(` - ${key}`); + }); + } + } + + // Print type errors + for (const locale in stats.typeErrors) { + const typeErrors = stats.typeErrors[locale]; + if (typeErrors.length > 0) { + hasIssues = true; + console.log(`โš ๏ธ ${locale} has ${typeErrors.length} type mismatches:`); + typeErrors.forEach(err => { + console.log(` - ${err.key}: expected ${err.refType}, got ${err.localeType}`); + }); + } + } + + // Print empty values check if needed + console.log('\n๐Ÿ“Š Checking for empty values...'); + for (const locale of locales) { + checkEmptyValues(translations[locale], locale); + } + + if (!hasIssues) { + console.log('โœ… All locales have consistent structure!'); + } + + return hasIssues; +} + +// Function to check for empty values +function checkEmptyValues(obj, locale, prefix = '') { + for (const key in obj) { + const fullKey = prefix ? `${prefix}.${key}` : key; + const value = obj[key]; + + if (value === '') { + console.log(`โš ๏ธ ${locale} has empty string at ${fullKey}`); + } else if (value === null) { + console.log(`โš ๏ธ ${locale} has null value at ${fullKey}`); + } else if (typeof value === 'object' && !Array.isArray(value)) { + checkEmptyValues(value, locale, fullKey); + } + } +} + +// Main function +function main() { + // Create compiled directory if it doesn't exist + fs.ensureDirSync(COMPILED_DIR); + + // Run validation + validateTranslations(); + + // Find unlocalized text + findUnlocalizedText(); +} + +// Watch mode +if (process.argv.includes('--watch')) { + console.log('๐Ÿ‘€ Watching for changes...'); + + let debounceTimer; + const debounceDelay = 100; + + // Initial validation + clearTimeout(debounceTimer); + debounceTimer = setTimeout(() => { + main(); + }, debounceDelay); + + // Watch for changes in the compiled directory + chokidar.watch(COMPILED_DIR, { + ignoreInitial: true, + ignored: /.*~$/, // ะ˜ะณะฝะพั€ะธั€ะพะฒะฐั‚ัŒ ัะบั€ั‹ั‚ั‹ะต ั„ะฐะนะปั‹ + }).on('all', (event, path) => { + if (event === 'change' || event === 'add' || event === 'unlink') { + clearTimeout(debounceTimer); + debounceTimer = setTimeout(() => { + console.log(`๐Ÿ” Detected changes in ${path} (${event}), revalidating...`); + main(); + }, debounceDelay); + } + }); +} else { + // Run once + main(); +} diff --git a/src/systems/rendering/userStats.js b/src/systems/rendering/userStats.js index f44d9ac..7d98937 100644 --- a/src/systems/rendering/userStats.js +++ b/src/systems/rendering/userStats.js @@ -213,7 +213,7 @@ export function renderUserStats() { if (!lastGeneratedData.userStats && !committedTrackerData.userStats) { // Always render to the #rpg-user-stats container (mobile layout just moves it around in DOM) - $userStatsContainer.html('
No statuses generated yet
'); + $userStatsContainer.html('
No statuses generated yet
'); return; } diff --git a/src/systems/ui/trackerEditor.js b/src/systems/ui/trackerEditor.js index e84c75b..f287847 100644 --- a/src/systems/ui/trackerEditor.js +++ b/src/systems/ui/trackerEditor.js @@ -58,14 +58,14 @@ function set_ids_names(list_with_stats, index, value) { if (extensionSettings.userStats && Object.prototype.hasOwnProperty.call(extensionSettings.userStats, oldId)) { extensionSettings.userStats[newId] = extensionSettings.userStats[oldId]; delete extensionSettings.userStats[oldId]; -function setIdsNames(listWithStats, index, value) { - listWithStats[index].name = value; - const ids = listWithStats.toSpliced(index, 1).map(stat => stat.id); - const snakeValue = safeToSnake(value); // new id format - if (snakeValue !== value && !ids.includes(snakeValue)) { // check if this id already exists - listWithStats[index].id = snakeValue; + } + + if (extensionSettings.classicStats && Object.prototype.hasOwnProperty.call(extensionSettings.classicStats, oldId)) { + extensionSettings.classicStats[newId] = extensionSettings.classicStats[oldId]; + delete extensionSettings.classicStats[oldId]; + } } - return listWithStats; + return list_with_stats; }