Skip to content

Commit

Permalink
feat: Add subtitles translation prevention
Browse files Browse the repository at this point in the history
🎯 Forces YouTube subtitles to stay in video's original language
- Use ASR track to detect original video language
- Apply manual track in same language if available
- Disable subtitles if only ASR track is available
  • Loading branch information
YouG-o committed Mar 2, 2025
1 parent 07ab2e0 commit e3212c3
Show file tree
Hide file tree
Showing 14 changed files with 249 additions and 8 deletions.
3 changes: 2 additions & 1 deletion manifest.chrome.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,8 @@
"web_accessible_resources": [{
"resources": [
"dist/content/audioTranslation/audioScript.js",
"dist/content/descriptionTranslation/descriptionScript.js"
"dist/content/descriptionTranslation/descriptionScript.js",
"dist/content/subtitlesTranslation/subtitlesScript.js"
],
"matches": ["*://*.youtube.com/*"]
}]
Expand Down
3 changes: 2 additions & 1 deletion manifest.firefox.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,8 @@
"web_accessible_resources": [{
"resources": [
"dist/content/audioTranslation/audioScript.js",
"dist/content/descriptionTranslation/descriptionScript.js"
"dist/content/descriptionTranslation/descriptionScript.js",
"dist/content/subtitlesTranslation/subtitlesScript.js"
],
"matches": ["*://*.youtube.com/*"]
}]
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
"watch:ts": "tsc --watch",
"watch:css": "tailwindcss -i ./src/styles/main.css -o ./dist/styles/main.css --watch",
"copy:assets": "cp -r assets/* dist/assets/",
"copy:scripts": "mkdir -p dist/content/audioTranslation dist/content/descriptionTranslation && cp src/content/audioTranslation/audioScript.js dist/content/audioTranslation/ && cp src/content/descriptionTranslation/descriptionScript.js dist/content/descriptionTranslation/",
"copy:scripts": "mkdir -p dist/content/audioTranslation dist/content/descriptionTranslation dist/content/subtitlesTranslation && cp src/content/audioTranslation/audioScript.js dist/content/audioTranslation/ && cp src/content/descriptionTranslation/descriptionScript.js dist/content/descriptionTranslation/ && cp src/content/subtitlesTranslation/subtitlesScript.js dist/content/subtitlesTranslation/",
"pre:web-ext:firefox": "cp manifest.firefox.json manifest.json",
"pre:web-ext:chrome": "cp manifest.chrome.json manifest.json",
"web-ext:firefox": "web-ext build --overwrite-dest -a web-ext-artifacts/firefox",
Expand Down
3 changes: 2 additions & 1 deletion src/config/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,6 @@
const DEFAULT_SETTINGS: ExtensionSettings = {
titleTranslation: true,
audioTranslation: true,
descriptionTranslation: true
descriptionTranslation: true,
subtitlesTranslation: false
};
21 changes: 21 additions & 0 deletions src/content/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,10 @@ async function initializeFeatures() {
initializeDescriptionTranslation();
setupDescriptionObserver();
}
if (currentSettings?.subtitlesTranslation) {
initializeSubtitlesTranslation();
setupSubtitlesObserver();
}
}

// Initialize functions
Expand Down Expand Up @@ -83,6 +87,23 @@ function initializeDescriptionTranslation() {
});
}

function initializeSubtitlesTranslation() {
subtitlesLog('Initializing subtitles translation prevention');

if (currentSettings?.subtitlesTranslation) {
handleSubtitlesTranslation();
}

browser.runtime.onMessage.addListener((message: unknown) => {
if (isToggleMessage(message) && message.feature === 'subtitles') {
if (message.isEnabled) {
handleSubtitlesTranslation();
}
}
return true;
});
}

// Listen for toggle changes
browser.runtime.onMessage.addListener((message: unknown) => {
if (isToggleMessage(message)) {
Expand Down
7 changes: 6 additions & 1 deletion src/content/logging.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,10 @@ const LOG_STYLES = {
CORE: {
context: '[Core]',
color: '#c084fc' // light purple
},
SUBTITLES: {
context: '[Subtitles]',
color: '#FF9800' // orange
}
} as const;

Expand All @@ -53,4 +57,5 @@ const otherTitlesLog = createLogger(LOG_STYLES.OTHER_TITLES);
const audioLog = createLogger(LOG_STYLES.AUDIO);
const descriptionLog = createLogger(LOG_STYLES.DESCRIPTION);
const coreLog = createLogger(LOG_STYLES.CORE);
const titlesLog = createLogger(LOG_STYLES.TITLES);
const titlesLog = createLogger(LOG_STYLES.TITLES);
const subtitlesLog = createLogger(LOG_STYLES.SUBTITLES);
24 changes: 24 additions & 0 deletions src/content/observer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,30 @@ function setupMainTitleObserver() {
}


// SUBTITLES OBSERVERS --------------------------------------------------------------------
let subtitlesObserver: MutationObserver | null = null;

function setupSubtitlesObserver() {
waitForElement('ytd-watch-flexy').then((watchFlexy) => {
subtitlesObserver = new MutationObserver((mutations) => {
for (const mutation of mutations) {
if (mutation.type === 'attributes' && mutation.attributeName === 'video-id') {
// Wait for movie_player before injecting script
waitForElement('#movie_player').then(() => {
handleSubtitlesTranslation();
});
}
}
});

subtitlesObserver.observe(watchFlexy, {
attributes: true,
attributeFilter: ['video-id']
});
});
}


// OTHER TITLES OBSERVER -----------------------------------------------------------
let homeObserver: MutationObserver | null = null;
let recommendedObserver: MutationObserver | null = null;
Expand Down
29 changes: 29 additions & 0 deletions src/content/subtitlesTranslation/subtitlesIndex.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/*
* Copyright (C) 2025-present YouGo (https://github.com/youg-o)
* This program is licensed under the GNU Affero General Public License v3.0.
* You may redistribute it and/or modify it under the terms of the license.
*
* Attribution must be given to the original author.
* This program is distributed without any warranty; see the license for details.
*/

/**
* Handles YouTube's subtitles selection to force original language
*
* YouTube provides different types of subtitle tracks:
* - ASR (Automatic Speech Recognition) tracks: Always in original video language
* - Manual tracks: Can be original or translated
* - Translated tracks: Generated from manual tracks
*
* Strategy:
* 1. Find ASR track to determine original video language
* 2. Look for manual track in same language
* 3. Apply original language track if found
*/

async function handleSubtitlesTranslation() {
subtitlesLog('Initializing subtitles translation prevention');
const script = document.createElement('script');
script.src = browser.runtime.getURL('dist/content/subtitlesTranslation/subtitlesScript.js');
document.documentElement.appendChild(script);
}
103 changes: 103 additions & 0 deletions src/content/subtitlesTranslation/subtitlesScript.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
/*
* Copyright (C) 2025-present YouGo (https://github.com/youg-o)
* This program is licensed under the GNU Affero General Public License v3.0.
* You may redistribute it and/or modify it under the terms of the license.
*
* Attribution must be given to the original author.
* This program is distributed without any warranty; see the license for details.
*/

(() => {
const LOG_PREFIX = '[YNT]';
const LOG_STYLES = {
SUBTITLES: { context: '[SUBTITLES]', color: '#FF9800' }
};

function createLogger(category) {
return (message, ...args) => {
console.log(
`%c${LOG_PREFIX}${category.context} ${message}`,
`color: ${category.color}`,
...args
);
};
}

const subtitlesLog = createLogger(LOG_STYLES.SUBTITLES);

function setOriginalSubtitles() {
const player = document.getElementById('movie_player');
if (!player) return false;

try {
// Get current track - if no track, subtitles are disabled
const currentTrack = player.getOption('captions', 'track');
if (!currentTrack) return true; // No captions enabled, nothing to do

// Get video response to access caption tracks
const response = player.getPlayerResponse();
const captionTracks = response.captions?.playerCaptionsTracklistRenderer?.captionTracks;
if (!captionTracks) return false;

// Find ASR track to determine original language
const asrTrack = captionTracks.find(track => track.kind === 'asr');
if (!asrTrack) return false;

// Find manual track in original language
const originalTrack = captionTracks.find(track =>
track.languageCode === asrTrack.languageCode && !track.kind
);

// If no manual track in original language exists
if (!originalTrack) {
subtitlesLog('No manual track in original language, disabling subtitles');
player.setOption('captions', 'track', {}); // Disable subtitles
return true;
}

// If current track is already the original manual track, do nothing
if (currentTrack.languageCode === asrTrack.languageCode && !currentTrack.kind) {
subtitlesLog(`Subtitles are already in original language: "${currentTrack.languageName}"`);
return true;
}

// Apply original manual track
subtitlesLog(`Setting subtitles from "${currentTrack.languageName}" to original language "${originalTrack.name.simpleText}"`);
player.setOption('captions', 'track', originalTrack);
return true;

} catch (error) {
subtitlesLog('Error:', error);
return false;
}
}

const player = document.getElementById('movie_player');
if (player) {
let processingVideoId = null;
let initialSetupDone = false;

player.addEventListener('onVideoDataChange', () => {
const videoId = new URLSearchParams(window.location.search).get('v');
if (videoId === processingVideoId) return;

processingVideoId = videoId;
subtitlesLog('Video data changed, checking subtitles...');

const success = setOriginalSubtitles();
if (success) {
processingVideoId = null;
} else if (!initialSetupDone) {
initialSetupDone = true;
setTimeout(() => {
setOriginalSubtitles();
processingVideoId = null;
}, 200);
}
});

// Initial setup
setOriginalSubtitles();
initialSetupDone = true;
}
})();
7 changes: 6 additions & 1 deletion src/content/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,12 @@ function isToggleMessage(message: unknown): message is Message {
'action' in message &&
message.action === 'toggleTranslation' &&
'feature' in message &&
(message.feature === 'titles' || message.feature === 'audio') &&
(
message.feature === 'titles' ||
message.feature === 'audio' ||
message.feature === 'description' ||
message.feature === 'subtitles'
) &&
'isEnabled' in message &&
typeof message.isEnabled === 'boolean'
);
Expand Down
13 changes: 13 additions & 0 deletions src/popup/popup.html
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,19 @@ <h2 class="text-sm font-medium text-white">Descriptions</h2>
Prevent automatic translation of video descriptions
</p>
</div>
<!-- Original Subtitles Feature -->
<div class="p-3 rounded-lg border border-gray-600 hover:bg-gray-600 transition-colors">
<div class="flex items-center justify-between mb-2">
<h2 class="text-sm font-medium text-white">Subtitles</h2>
<label class="relative inline-flex items-center cursor-pointer">
<input type="checkbox" class="sr-only peer" id="subtitlesTranslation">
<div class="w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-blue-300 rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-blue-600"></div>
</label>
</div>
<p class="text-sm text-gray-400">
Display original subtitles if available (only real ones not auto-generated)
</p>
</div>
</div>
</main>
<footer class="flex flex-col gap-4 pt-4 border-t border-gray-600">
Expand Down
38 changes: 37 additions & 1 deletion src/popup/popup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
const titleToggle = document.getElementById('titleTranslation') as HTMLInputElement;
const audioToggle = document.getElementById('audioTranslation') as HTMLInputElement;
const descriptionToggle = document.getElementById('descriptionTranslation') as HTMLInputElement;
const subtitlesToggle = document.getElementById('subtitlesTranslation') as HTMLInputElement;

// Initialize toggle states from storage
document.addEventListener('DOMContentLoaded', async () => {
Expand All @@ -23,12 +24,14 @@ document.addEventListener('DOMContentLoaded', async () => {
settings: {
titleTranslation: true,
audioTranslation: true,
descriptionTranslation: true
descriptionTranslation: true,
subtitlesTranslation: false
}
});
titleToggle.checked = true;
audioToggle.checked = true;
descriptionToggle.checked = true;
subtitlesToggle.checked = false;
return;
}

Expand All @@ -39,6 +42,7 @@ document.addEventListener('DOMContentLoaded', async () => {
titleToggle.checked = settings.titleTranslation;
audioToggle.checked = settings.audioTranslation;
descriptionToggle.checked = settings.descriptionTranslation;
subtitlesToggle.checked = settings.subtitlesTranslation;

console.log(
'[NTM-Debug] Settings loaded - Title translation prevention is: %c%s',
Expand All @@ -55,6 +59,11 @@ document.addEventListener('DOMContentLoaded', async () => {
settings.descriptionTranslation ? 'color: green; font-weight: bold' : 'color: red; font-weight: bold',
settings.descriptionTranslation ? 'ON' : 'OFF'
);
console.log(
'[NTM-Debug] Settings loaded - Subtitles translation prevention is: %c%s',
settings.subtitlesTranslation ? 'color: green; font-weight: bold' : 'color: red; font-weight: bold',
settings.subtitlesTranslation ? 'ON' : 'OFF'
);
} catch (error) {
console.error('Load error:', error);
}
Expand Down Expand Up @@ -157,4 +166,31 @@ descriptionToggle.addEventListener('change', async () => {
} catch (error) {
console.error('Save error:', error);
}
});

// Handle subtitles toggle changes
subtitlesToggle.addEventListener('change', async () => {
const isEnabled = subtitlesToggle.checked;

try {
const data = await browser.storage.local.get('settings');
const settings = data.settings as ExtensionSettings;

await browser.storage.local.set({
settings: {
...settings,
subtitlesTranslation: isEnabled
}
});

// Send message to content script
await browser.tabs.query({ active: true, currentWindow: true }).then((tabs) => {
browser.tabs.sendMessage(tabs[0].id!, {
feature: 'subtitles',
isEnabled
} as Message);
});
} catch (error) {
console.error('Save error:', error);
}
});
3 changes: 2 additions & 1 deletion src/types/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@

interface Message {
action: 'toggleTranslation';
feature: 'titles' | 'audio' | 'description';
feature: 'titles' | 'audio' | 'description' | 'subtitles';
isEnabled: boolean;
}

Expand All @@ -38,6 +38,7 @@ interface ExtensionSettings {
titleTranslation: boolean;
audioTranslation: boolean;
descriptionTranslation: boolean;
subtitlesTranslation: boolean;
}

interface YouTubePlayerResponse {
Expand Down
1 change: 1 addition & 0 deletions tsconfig.content.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
"src/content/titleTranslation/otherTitles.ts",
"src/content/audioTranslation/audioIndex.ts",
"src/content/descriptionTranslation/descriptionIndex.ts",
"src/content/subtitlesTranslation/subtitlesIndex.ts",
"src/content/observer.ts",
"src/content/index.ts"
]
Expand Down

0 comments on commit e3212c3

Please sign in to comment.