Skip to content

Commit

Permalink
Add option to use capital case for non english titles
Browse files Browse the repository at this point in the history
The current check leans towards english and only treats it as non english if the language detector thinks the result is viable, which only occurs for longer titles. However, it is not accurate enough to be enabled by default
  • Loading branch information
ajayyy committed Dec 19, 2023
1 parent 2841da1 commit 6ef2376
Show file tree
Hide file tree
Showing 7 changed files with 108 additions and 17 deletions.
2 changes: 1 addition & 1 deletion public/_locales
Submodule _locales updated 1 files
+7 −4 en/messages.json
14 changes: 14 additions & 0 deletions public/options/options.html
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,20 @@
</div>
</div>

<div data-type="toggle" data-sync="onlyTitleCaseInEnglish" data-dependent-on-selector="titleFormatting" data-dependent-on-selector-value="1">
<div class="sb-switch-container">
<label class="sb-switch">
<input id="onlyTitleCaseInEnglish" type="checkbox" checked>
<span class="sb-slider sb-round"></span>
</label>
<label class="sb-switch-label" for="onlyTitleCaseInEnglish">
__MSG_onlyTitleCaseInEnglish__
</label>
</div>

<div class="small-description">__MSG_onlyTitleCaseInEnglishDescription__</div>
</div>

<div data-type="selector" data-sync="thumbnailFallback">
<label class="optionLabel" for="thumbnailFallback">__MSG_thumbnailFallbackOption__:</label>

Expand Down
2 changes: 2 additions & 0 deletions src/config/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ interface SBConfig {
keepUnsubmittedInPrivate: boolean;
titleFormatting: TitleFormatting;
shouldCleanEmojis: boolean;
onlyTitleCaseInEnglish: boolean;
serverAddress: string;
thumbnailServerAddress: string;
fetchTimeout: number;
Expand Down Expand Up @@ -157,6 +158,7 @@ const syncDefaults = {
keepUnsubmittedInPrivate: false,
titleFormatting: isEnglish ? TitleFormatting.TitleCase : TitleFormatting.Disable,
shouldCleanEmojis: true,
onlyTitleCaseInEnglish: false,
serverAddress: CompileConfig.serverAddress,
thumbnailServerAddress: CompileConfig.thumbnailServerAddress,
fetchTimeout: 7000,
Expand Down
25 changes: 22 additions & 3 deletions src/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,9 +97,14 @@ async function init() {
if (dependentOn)
isDependentOnReversed = dependentOn.getAttribute("data-toggle-type") === "reverse" || optionsElements[i].getAttribute("data-dependent-on-inverted") === "true";

if (await shouldHideOption(optionsElements[i]) || (dependentOn && (isDependentOnReversed ? Config.config![dependentOnName] : !Config.config![dependentOnName]))) {
const dependOnSelectorName = optionsElements[i].getAttribute("data-dependent-on-selector");
const dependOnSelectorValue = optionsElements[i].getAttribute("data-dependent-on-selector-value");

if (await shouldHideOption(optionsElements[i])
|| (dependentOn && (isDependentOnReversed ? Config.config![dependentOnName] : !Config.config![dependentOnName]))
|| (dependOnSelectorName && dependOnSelectorValue && String(Config.config![dependOnSelectorName]) !== dependOnSelectorValue)) {
optionsElements[i].classList.add("hidden", "hiding");
if (!dependentOn)
if (!dependentOn && !dependOnSelectorName)
continue;
}

Expand Down Expand Up @@ -296,11 +301,25 @@ async function init() {
const selectorElement = optionsElements[i].querySelector(".selector-element") as HTMLSelectElement;
selectorElement.value = configValue;

selectorElement.addEventListener("change", () => {
// eslint-disable-next-line @typescript-eslint/no-misused-promises
selectorElement.addEventListener("change", async () => {
let value: string | number = selectorElement.value;
if (!isNaN(Number(value))) value = Number(value);

Config.config![option] = value;

// If other options depend on this, hide/show them
const dependents = optionsContainer.querySelectorAll(`[data-dependent-on-selector='${option}']`);
for (let j = 0; j < dependents.length; j++) {
const dependOnValue = dependents[j].getAttribute("data-dependent-on-selector-value");
if (!await shouldHideOption(dependents[j]) && String(value) === dependOnValue) {
dependents[j].classList.remove("hidden");
setTimeout(() => dependents[j].classList.remove("hiding"), 1);
} else {
dependents[j].classList.add("hiding");
setTimeout(() => dependents[j].classList.add("hidden"), 400);
}
}
});
break;
}
Expand Down
20 changes: 19 additions & 1 deletion src/popup/FormattingOptionsComponent.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import * as React from "react";
import Config, { ThumbnailFallbackOption } from "../config/config";
import Config, { ThumbnailFallbackOption, TitleFormatting } from "../config/config";
import { toFirstLetterUppercase, toLowerCaseTitle, toSentenceCase } from "../titles/titleFormatter";
import { SelectOptionComponent } from "./SelectOptionComponent";
import { ToggleOptionComponent } from "./ToggleOptionComponent";

export const FormattingOptionsComponent = () => {
const [titleFormatting, setTitleFormatting] = React.useState(String(Config.config!.titleFormatting));
const [shouldCleanEmojis, setShouldCleanEmojis] = React.useState(Config.config!.shouldCleanEmojis);
const [onlyTitleCaseInEnglish, setOnlyTitleCaseInEnglish] = React.useState(Config.config!.onlyTitleCaseInEnglish);
const [thumbnailFallback, setThumbnailFallback] = React.useState(String(Config.config!.thumbnailFallback));
const [thumbnailFallbackAutogenerated, setThumbnailFallbackAutogenerated] = React.useState(String(Config.config!.thumbnailFallbackAutogenerated));

Expand Down Expand Up @@ -58,6 +59,23 @@ export const FormattingOptionsComponent = () => {
value={shouldCleanEmojis}
label={chrome.i18n.getMessage("shouldCleanEmojis")}
/>
{
titleFormatting === String(TitleFormatting.TitleCase) &&
<>
<ToggleOptionComponent
id="onlyTitleCaseInEnglish"
style={{
paddingTop: "15px"
}}
onChange={(value) => {
setOnlyTitleCaseInEnglish(value);
Config.config!.onlyTitleCaseInEnglish = value;
}}
value={onlyTitleCaseInEnglish}
label={chrome.i18n.getMessage("onlyTitleCaseInEnglish")}
/>
</>
}

{/* Thumbnail Fallback Option */}
<SelectOptionComponent
Expand Down
61 changes: 49 additions & 12 deletions src/titles/titleFormatter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ export async function toSentenceCase(str: string, isCustom: boolean): Promise<st
export async function toTitleCase(str: string, isCustom: boolean): Promise<string> {
const words = str.split(" ");
const mostlyAllCaps = isMostlyAllCaps(words);
const { isGreek, isTurkiq } = await getLangInfo(str);
const { isGreek, isTurkiq, isEnglish } = await getLangInfo(str);

let result = "";
let index = 0;
Expand All @@ -150,7 +150,8 @@ export async function toTitleCase(str: string, isCustom: boolean): Promise<strin
// For custom titles, allow any not just first capital
// For non-custom, allow any that isn't all caps
result += word + " ";
} else if (!startOfSentence(index, words) && listHasWord(titleCaseNotCapitalized, word.toLowerCase())) {
} else if ((!Config.config!.onlyTitleCaseInEnglish || isEnglish)

Check failure on line 153 in src/titles/titleFormatter.ts

View workflow job for this annotation

GitHub Actions / Run tests

toTitleCase › Go on the table with a cat

TypeError: Cannot read properties of undefined (reading 'onlyTitleCaseInEnglish') at src/titles/titleFormatter.ts:153:37 at fulfilled (src/titles/titleFormatter.ts:5:58)

Check failure on line 153 in src/titles/titleFormatter.ts

View workflow job for this annotation

GitHub Actions / Run tests

toTitleCase › Go On The Table With A Cat

TypeError: Cannot read properties of undefined (reading 'onlyTitleCaseInEnglish') at src/titles/titleFormatter.ts:153:37 at fulfilled (src/titles/titleFormatter.ts:5:58)

Check failure on line 153 in src/titles/titleFormatter.ts

View workflow job for this annotation

GitHub Actions / Run tests

toTitleCase › 5 Minute Timer [MOUSE MAZE] 🐭

TypeError: Cannot read properties of undefined (reading 'onlyTitleCaseInEnglish') at src/titles/titleFormatter.ts:153:37 at fulfilled (src/titles/titleFormatter.ts:5:58)

Check failure on line 153 in src/titles/titleFormatter.ts

View workflow job for this annotation

GitHub Actions / Run tests

toTitleCase › AWESOME ART TRICKS and EASY DRAWING HACKS

TypeError: Cannot read properties of undefined (reading 'onlyTitleCaseInEnglish') at src/titles/titleFormatter.ts:153:37 at fulfilled (src/titles/titleFormatter.ts:5:58)

Check failure on line 153 in src/titles/titleFormatter.ts

View workflow job for this annotation

GitHub Actions / Run tests

toTitleCase › 5 min countdown timer (roller coaster) 🎢

TypeError: Cannot read properties of undefined (reading 'onlyTitleCaseInEnglish') at src/titles/titleFormatter.ts:153:37 at fulfilled (src/titles/titleFormatter.ts:5:58)

Check failure on line 153 in src/titles/titleFormatter.ts

View workflow job for this annotation

GitHub Actions / Run tests

toTitleCase › 5 min COUNTDOWN timer from U.S.A (roller coaster) 🎢

TypeError: Cannot read properties of undefined (reading 'onlyTitleCaseInEnglish') at src/titles/titleFormatter.ts:153:37 at fulfilled (src/titles/titleFormatter.ts:5:58)

Check failure on line 153 in src/titles/titleFormatter.ts

View workflow job for this annotation

GitHub Actions / Run tests

toTitleCase › Going somewhere [U.S.A is the place]

TypeError: Cannot read properties of undefined (reading 'onlyTitleCaseInEnglish') at src/titles/titleFormatter.ts:153:37 at fulfilled (src/titles/titleFormatter.ts:5:58)

Check failure on line 153 in src/titles/titleFormatter.ts

View workflow job for this annotation

GitHub Actions / Run tests

toTitleCase › The car is from the U.S.A

TypeError: Cannot read properties of undefined (reading 'onlyTitleCaseInEnglish') at src/titles/titleFormatter.ts:153:37 at fulfilled (src/titles/titleFormatter.ts:5:58)

Check failure on line 153 in src/titles/titleFormatter.ts

View workflow job for this annotation

GitHub Actions / Run tests

toTitleCase › When I WENT TO The Store

TypeError: Cannot read properties of undefined (reading 'onlyTitleCaseInEnglish') at src/titles/titleFormatter.ts:153:37 at fulfilled (src/titles/titleFormatter.ts:5:58)

Check failure on line 153 in src/titles/titleFormatter.ts

View workflow job for this annotation

GitHub Actions / Run tests

toTitleCase › Something happened in the 2000s

TypeError: Cannot read properties of undefined (reading 'onlyTitleCaseInEnglish') at src/titles/titleFormatter.ts:153:37 at fulfilled (src/titles/titleFormatter.ts:5:58)
&& !startOfSentence(index, words) && listHasWord(titleCaseNotCapitalized, word.toLowerCase())) {
// Skip lowercase check for the first word
result += await toLowerCase(word, isTurkiq) + " ";
} else if (isFirstLetterCapital(word) &&
Expand Down Expand Up @@ -352,36 +353,72 @@ async function toUpperCase(word: string, isTurkiq: boolean): Promise<string> {
async function getLangInfo(str: string): Promise<{
isGreek: boolean;
isTurkiq: boolean;
isEnglish: boolean;
}> {
const result = await checkLanguages(str, ["el", "tr", "az"], 30);
if (str.split(" ").length > 1) {
// Remove hashtags
str = str.replace(/#[^\s]+/g, "").trim();
}

const threshold = 30;
const result = await checkLanguages(str, ["el", "tr", "az", "en"], threshold);

return {
isGreek: result[0],
isTurkiq: result[1] || result[2]
isGreek: result.results[0],
isTurkiq: result.results[1] || result.results[2],

// Not english if it detects no english, it is reliable, and the top language is the same when one word is removed
// Helps remove false positives
isEnglish: !(!result.results[3]
&& result.isReliable
&& Config.config!.onlyTitleCaseInEnglish // Only do further checks if enabled
&& result.topLanguage === ((await checkLanguages(str.replace(/[^ ]+$/, ""), [], threshold)).topLanguage))
}
}

async function checkAnyLanguage(title: string, languages: string[], percentage: number): Promise<boolean> {
return (await checkLanguages(title, languages, percentage)).every((v) => v);
return (await checkLanguages(title, languages, percentage)).results.every((v) => v);
}

async function checkLanguages(title: string, languages: string[], percentage: number): Promise<boolean[]> {
if (!cld && (typeof chrome === "undefined" || !("detectLanguage" in chrome.i18n))) return languages.map(() => false);
async function checkLanguages(title: string, languages: string[], percentage: number): Promise<{
results: boolean[];
topLanguage?: string | null;
isReliable: boolean;
}> {
if (!cld && (typeof chrome === "undefined" || !("detectLanguage" in chrome.i18n))) {
return {
results: languages.map(() => false),
isReliable: false
};
}

try {
const getLanguageFromBrowserApi = async (title: string) => {
const result = await chromeP.i18n.detectLanguage(title);
return result.languages.map((l) => ({...l, isReliable: result.isReliable}));
};

const detectedLanguages = cld
? [(await (await cld).findLanguage(title))].map((l) => ({ language: l.language, percentage: l.probability * 100 }))
: (await chromeP.i18n.detectLanguage(title)).languages;
? [(await (await cld).findLanguage(title))].map((l) => ({ language: l.language,
percentage: l.probability * 100, isReliable: l.is_reliable }))
: await getLanguageFromBrowserApi(title);

const result: boolean[] = [];
for (const language of languages) {
const matchingLanguage = detectedLanguages.find((l) => l.language === language);
result.push(!!matchingLanguage && matchingLanguage.percentage > percentage);
}

return result;
return {
results: result,
topLanguage: detectedLanguages[0]?.language,
isReliable: detectedLanguages.some((l) => l.isReliable)
};
} catch (e) {
return languages.map(() => false);
return {
results: languages.map(() => false),
isReliable: false
};
}
}

Expand Down
1 change: 1 addition & 0 deletions src/videoBranding/videoBranding.ts
Original file line number Diff line number Diff line change
Expand Up @@ -410,6 +410,7 @@ export function setupOptionChangeListener(): void {
"useCrowdsourcedTitles",
"titleFormatting",
"shouldCleanEmojis",
"onlyTitleCaseInEnglish",
"thumbnailFallback",
"thumbnailFallbackAutogenerated",
"alwaysShowShowOriginalButton",
Expand Down

0 comments on commit 6ef2376

Please sign in to comment.