chore: add translation cleanup tooling and remove unused keys

- Add check-unused-translations.js script for detecting unused i18n keys
- Remove unused player keys: refresh_tracks, audio_tracks, playback_state, index
- Remove unused aspect_ratio section (7 keys)
- Update copilot-instructions.md with i18n guidelines (only edit en.json, Crowdin handles other languages)
This commit is contained in:
Uruk
2026-01-14 14:51:42 +01:00
parent 376d2e84da
commit 895c245254
2 changed files with 122 additions and 39 deletions

View File

@@ -0,0 +1,121 @@
#!/usr/bin/env node
/**
* Check for unused translation keys in en.json
* Usage: bun run scripts/check-unused-translations.js [--remove]
*/
const fs = require("node:fs");
const path = require("node:path");
const { execSync } = require("node:child_process");
const TRANSLATION_FILE = path.join(__dirname, "../translations/en.json");
const REMOVE_UNUSED = process.argv.includes("--remove");
// Read translation file
const translations = JSON.parse(fs.readFileSync(TRANSLATION_FILE, "utf8"));
// Flatten nested keys
function flattenKeys(obj, prefix = "") {
let keys = [];
for (const [key, value] of Object.entries(obj)) {
const fullKey = prefix ? `${prefix}.${key}` : key;
if (typeof value === "object" && value !== null && !Array.isArray(value)) {
keys = keys.concat(flattenKeys(value, fullKey));
} else {
keys.push(fullKey);
}
}
return keys;
}
// Search for key usage in codebase
function isKeyUsed(key) {
try {
// Escape special regex characters in the key
const escapedKey = key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
// Search in TypeScript/TSX files
const result = execSync(
`git grep -l "${escapedKey}" -- "*.ts" "*.tsx" 2>nul || echo ""`,
{
encoding: "utf8",
cwd: path.join(__dirname, ".."),
maxBuffer: 10 * 1024 * 1024,
},
).trim();
return result.length > 0;
} catch (_error) {
// If grep fails, assume key is used to be safe
return true;
}
}
// Remove nested key from object
function removeNestedKey(obj, keyPath) {
const keys = keyPath.split(".");
const lastKey = keys.pop();
let current = obj;
for (const key of keys) {
if (!current[key]) return false;
current = current[key];
}
if (current[lastKey] !== undefined) {
delete current[lastKey];
// Clean up empty parent objects
if (Object.keys(current).length === 0 && keys.length > 0) {
removeNestedKey(obj, keys.join("."));
}
return true;
}
return false;
}
console.log("🔍 Checking for unused translation keys...\n");
const allKeys = flattenKeys(translations);
const unusedKeys = [];
for (const key of allKeys) {
if (!isKeyUsed(key)) {
unusedKeys.push(key);
}
}
if (unusedKeys.length === 0) {
console.log("✅ All translation keys are being used!");
process.exit(0);
}
console.log(`Found ${unusedKeys.length} unused translation keys:\n`);
for (const key of unusedKeys) {
console.log(`${key}`);
}
if (REMOVE_UNUSED) {
console.log("\n🗑 Removing unused keys...");
let removed = 0;
for (const key of unusedKeys) {
if (removeNestedKey(translations, key)) {
removed++;
}
}
// Write back to file
fs.writeFileSync(
TRANSLATION_FILE,
`${JSON.stringify(translations, null, 2)}\n`,
"utf8",
);
console.log(`✅ Removed ${removed} unused translation keys from en.json`);
} else {
console.log("\n💡 Run with --remove flag to remove these keys from en.json");
console.log(
" Example: bun run scripts/check-unused-translations.js --remove",
);
}

View File

@@ -253,29 +253,7 @@
},
"subtitle_color": "Subtitle Color",
"subtitle_background_color": "Background Color",
"subtitle_font": "Subtitle Font",
"ksplayer_title": "KSPlayer Settings",
"hardware_decode": "Hardware Decoding",
"hardware_decode_description": "Use hardware acceleration for video decoding. Disable if you experience playback issues."
},
"vlc_subtitles": {
"title": "VLC Subtitle Settings",
"hint": "Customize subtitle appearance for VLC player. Changes take effect on next playback.",
"text_color": "Text Color",
"background_color": "Background Color",
"background_opacity": "Background Opacity",
"outline_color": "Outline Color",
"outline_opacity": "Outline Opacity",
"outline_thickness": "Outline Thickness",
"bold": "Bold Text",
"margin": "Bottom Margin"
},
"video_player": {
"title": "Video Player",
"video_player": "Video Player",
"video_player_description": "Choose which video player to use on iOS.",
"ksplayer": "KSPlayer",
"vlc": "VLC"
"subtitle_font": "Subtitle Font"
},
"other": {
"other_title": "Other",
@@ -294,11 +272,6 @@
"UNKNOWN": "Unknown"
},
"safe_area_in_controls": "Safe Area in Controls",
"video_player": "Video Player",
"video_players": {
"VLC_3": "VLC 3",
"VLC_4": "VLC 4 (Experimental + PiP)"
},
"show_custom_menu_links": "Show Custom Menu Links",
"show_large_home_carousel": "Show Large Home Carousel (beta)",
"hide_libraries": "Hide Libraries",
@@ -599,10 +572,6 @@
"could_not_create_stream_for_chromecast": "Could not create a stream for Chromecast",
"message_from_server": "Message from Server: {{message}}",
"next_episode": "Next Episode",
"refresh_tracks": "Refresh Tracks",
"audio_tracks": "Audio Tracks:",
"playback_state": "Playback State:",
"index": "Index:",
"continue_watching": "Continue Watching",
"go_back": "Go Back",
"downloaded_file_title": "You have this file downloaded",
@@ -610,13 +579,6 @@
"downloaded_file_yes": "Yes",
"downloaded_file_no": "No",
"downloaded_file_cancel": "Cancel",
"aspect_ratio_title": "Aspect Ratio",
"aspect_ratio_original": "Original",
"aspect_ratio_original_description": "Use video's original aspect ratio",
"aspect_ratio_16_9_description": "Widescreen (most common)",
"aspect_ratio_4_3_description": "Traditional TV format",
"aspect_ratio_1_1_description": "Square format",
"aspect_ratio_21_9_description": "Ultra-wide cinematic",
"playback_options_title": "Playback Options",
"mpv_subtitle_settings_title": "MPV Subtitle Settings",
"mpv_subtitle_settings_description": "Advanced subtitle customization for MPV player",