@@ -24,3 +24,4 @@ node_modules/
|
|||||||
|
|
||||||
# Claude
|
# Claude
|
||||||
CLAUDE.md
|
CLAUDE.md
|
||||||
|
yarn.lock
|
||||||
|
|||||||
+1
-1
@@ -6,6 +6,6 @@
|
|||||||
"js": "index.js",
|
"js": "index.js",
|
||||||
"css": "style.css",
|
"css": "style.css",
|
||||||
"author": "Marinara",
|
"author": "Marinara",
|
||||||
"version": "3.7.2",
|
"version": "3.7.3",
|
||||||
"homePage": "https://github.com/SpicyMarinara/rpg-companion-sillytavern"
|
"homePage": "https://github.com/SpicyMarinara/rpg-companion-sillytavern"
|
||||||
}
|
}
|
||||||
|
|||||||
Generated
+6
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"name": "rpg-companion-sillytavern",
|
||||||
|
"lockfileVersion": 3,
|
||||||
|
"requires": true,
|
||||||
|
"packages": {}
|
||||||
|
}
|
||||||
@@ -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": {}
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
@@ -213,7 +213,7 @@ export function renderUserStats() {
|
|||||||
|
|
||||||
if (!lastGeneratedData.userStats && !committedTrackerData.userStats) {
|
if (!lastGeneratedData.userStats && !committedTrackerData.userStats) {
|
||||||
// Always render to the #rpg-user-stats container (mobile layout just moves it around in DOM)
|
// Always render to the #rpg-user-stats container (mobile layout just moves it around in DOM)
|
||||||
$userStatsContainer.html('<div class="rpg-inventory-empty">No statuses generated yet</div>');
|
$userStatsContainer.html('<div class="rpg-inventory-empty" data-i18n-key="warnings.no-statuses">No statuses generated yet</div>');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ import { renderUserStats } from '../rendering/userStats.js';
|
|||||||
import { renderInfoBox } from '../rendering/infoBox.js';
|
import { renderInfoBox } from '../rendering/infoBox.js';
|
||||||
import { renderThoughts } from '../rendering/thoughts.js';
|
import { renderThoughts } from '../rendering/thoughts.js';
|
||||||
import { updateFabWidgets } from './mobile.js';
|
import { updateFabWidgets } from './mobile.js';
|
||||||
|
import { safeToSnake } from '../../utils/transformations.js';
|
||||||
|
|
||||||
let $editorModal = null;
|
let $editorModal = null;
|
||||||
let activeTab = 'userStats';
|
let activeTab = 'userStats';
|
||||||
@@ -38,6 +39,36 @@ let tempConfig = null; // Temporary config for cancel functionality
|
|||||||
let tempAssociation = null; // Temporary association state: { presetId: string|null, entityKey: string|null }
|
let tempAssociation = null; // Temporary association state: { presetId: string|null, entityKey: string|null }
|
||||||
let originalAssociation = null; // Original association when editor opened
|
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.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;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialize the tracker editor modal
|
* Initialize the tracker editor modal
|
||||||
*/
|
*/
|
||||||
@@ -885,7 +916,9 @@ function setupUserStatsListeners() {
|
|||||||
// Rename stat
|
// Rename stat
|
||||||
$('.rpg-stat-name').off('blur').on('blur', function () {
|
$('.rpg-stat-name').off('blur').on('blur', function () {
|
||||||
const index = $(this).data('index');
|
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
|
// Change stat max value
|
||||||
@@ -943,7 +976,9 @@ function setupUserStatsListeners() {
|
|||||||
// Rename attribute
|
// Rename attribute
|
||||||
$('.rpg-attr-name').off('blur').on('blur', function () {
|
$('.rpg-attr-name').off('blur').on('blur', function () {
|
||||||
const index = $(this).data('index');
|
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
|
// Enable/disable RPG Attributes section toggle
|
||||||
@@ -1394,7 +1429,9 @@ function setupPresentCharactersListeners() {
|
|||||||
// Rename field
|
// Rename field
|
||||||
$('.rpg-field-label').off('blur').on('blur', function () {
|
$('.rpg-field-label').off('blur').on('blur', function () {
|
||||||
const index = $(this).data('index');
|
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
|
// Update description
|
||||||
@@ -1443,7 +1480,9 @@ function setupPresentCharactersListeners() {
|
|||||||
// Rename character stat
|
// Rename character stat
|
||||||
$('.rpg-char-stat-label').off('blur').on('blur', function () {
|
$('.rpg-char-stat-label').off('blur').on('blur', function () {
|
||||||
const index = $(this).data('index');
|
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);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
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, '_')
|
||||||
|
// trim leading/trailing underscores
|
||||||
|
.replace(/^_+|_+$/g, '')
|
||||||
|
// finally, lowercase the result
|
||||||
|
.toLowerCase();
|
||||||
|
|
||||||
|
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
|
||||||
|
};
|
||||||
@@ -10732,7 +10732,10 @@ body[data-theme="cyberpunk"] .rpg-music-widget-play {
|
|||||||
|
|
||||||
/* Features row container */
|
/* Features row container */
|
||||||
.rpg-features-row {
|
.rpg-features-row {
|
||||||
display: flex;
|
display: grid;
|
||||||
|
grid-template-rows: repeat(2, 1fr);
|
||||||
|
grid-auto-flow: column;
|
||||||
|
grid-auto-columns: min-content;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
margin-bottom: 12px;
|
margin-bottom: 12px;
|
||||||
overflow-x: auto;
|
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 */
|
/* Center items when they fit, allow scrolling when they don't */
|
||||||
.rpg-features-row::before,
|
/*.rpg-features-row::before,*/
|
||||||
.rpg-features-row::after {
|
/*.rpg-features-row::after {*/
|
||||||
content: '';
|
/* content: '';*/
|
||||||
margin: auto;
|
/* margin: auto;*/
|
||||||
}
|
/*}*/
|
||||||
|
|
||||||
/* Hide scrollbar for cleaner look while maintaining functionality */
|
/* Hide scrollbar for cleaner look while maintaining functionality */
|
||||||
.rpg-features-row::-webkit-scrollbar {
|
.rpg-features-row::-webkit-scrollbar {
|
||||||
|
|||||||
Reference in New Issue
Block a user