Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/adapters/chrome/background/BackgroundServiceWorker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ export class BackgroundServiceWorker {
"background.prediction.completed",
`${predictions.length} predictions`,
);
if (!Array.isArray(predictions) || predictions.length === 0) {
if (predictions.length === 0) {
this.predictionManager.recordTraceTimelineEvent(
traceMeta,
"background.response.empty",
Expand Down
11 changes: 1 addition & 10 deletions src/adapters/chrome/background/CapitalizationHelper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,26 +14,18 @@ export interface CheckAutoCapitalizeParams {
autoCapitalize: boolean;
}

/**
* Checks if auto capitalization should be applied based on the input tokens and punctuation marks.
* @param params - Parameters for capitalization check
* @returns {Capitalization} The type of capitalization to be applied.
*/
export function checkAutoCapitalize({
lastWord,
wordCount,
newSentence,
endsWithSpace,
autoCapitalize,
}: CheckAutoCapitalizeParams): Capitalization {
const firstCharacterOfLastWord = lastWord.slice(0, 1);

// Whole word capitalization: " XYZ"
if (!endsWithSpace && lastWord && lastWord.length > 1 && lastWord === lastWord.toUpperCase()) {
return Capitalization.WholeWord;
}

// First letter capitalization: " Xyz"
const firstCharacterOfLastWord = lastWord.slice(0, 1);
if (
!endsWithSpace &&
isLetter(firstCharacterOfLastWord) &&
Expand All @@ -42,7 +34,6 @@ export function checkAutoCapitalize({
return Capitalization.FirstLetter;
}

// Auto capitalization after sentence-ending punctuation
if (
autoCapitalize &&
newSentence &&
Expand Down
108 changes: 71 additions & 37 deletions src/adapters/chrome/background/LanguageDetector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,47 +126,20 @@ export class LanguageDetector {
const priors = sanitizeAutoLanguageSitePriors(priorsRaw, allowedLanguages);
const sitePrior = getAutoLanguageSitePrior(priors, domain || undefined, allowedLanguages);
const key = this.getSessionKey(request);
const nextRuntimeGeneration =
typeof request.runtimeGeneration === "number" && Number.isFinite(request.runtimeGeneration)
? request.runtimeGeneration
: 0;
const nextRuntimeGeneration = this.resolveRuntimeGeneration(request.runtimeGeneration);
const session =
this.sessions.get(key) ||
({
this.createSessionState(
key,
tabId: request.tabId,
frameId: request.frameId,
suggestionId: request.suggestionId,
runtimeGeneration: nextRuntimeGeneration,
request,
nextRuntimeGeneration,
domain,
enabledLanguages: allowedLanguages.slice(),
stableLanguage: null,
resolvedLanguage: fallbackLanguage,
rollingSample: "",
pageLanguageHint: null,
pageLanguageHintResolved: false,
pendingLanguage: null,
pendingConfirmations: 0,
manualLockLanguage: null,
switchSuppressedUntilBoundary: false,
source: "fallback",
lastSeenAt: now,
priorEligible: false,
} as AutoLanguageSessionState);

const pageScopeChanged =
session.runtimeGeneration !== nextRuntimeGeneration || session.domain !== domain;
session.tabId = request.tabId;
session.frameId = request.frameId;
session.suggestionId = request.suggestionId;
session.runtimeGeneration = nextRuntimeGeneration;
session.domain = domain;
session.enabledLanguages = allowedLanguages.slice();
session.lastSeenAt = now;
if (pageScopeChanged) {
session.pageLanguageHint = null;
session.pageLanguageHintResolved = false;
}
allowedLanguages,
fallbackLanguage,
now,
);

this.syncSessionScope(session, request, nextRuntimeGeneration, domain, allowedLanguages, now);
session.rollingSample = updateAutoLanguageRollingSample(session.rollingSample, request.text);
const runtime = this.trackLiveRuntime({
tabId: request.tabId,
Expand Down Expand Up @@ -224,6 +197,67 @@ export class LanguageDetector {
};
}

private resolveRuntimeGeneration(runtimeGeneration: unknown): number {
return typeof runtimeGeneration === "number" && Number.isFinite(runtimeGeneration)
? runtimeGeneration
: 0;
}

private createSessionState(
key: string,
request: AutoLanguageRequest,
runtimeGeneration: number,
domain: string | null,
allowedLanguages: string[],
fallbackLanguage: string,
now: number,
): AutoLanguageSessionState {
return {
key,
tabId: request.tabId,
frameId: request.frameId,
suggestionId: request.suggestionId,
runtimeGeneration,
domain,
enabledLanguages: allowedLanguages.slice(),
stableLanguage: null,
resolvedLanguage: fallbackLanguage,
rollingSample: "",
pageLanguageHint: null,
pageLanguageHintResolved: false,
pendingLanguage: null,
pendingConfirmations: 0,
manualLockLanguage: null,
switchSuppressedUntilBoundary: false,
source: "fallback",
lastSeenAt: now,
priorEligible: false,
};
}

private syncSessionScope(
session: AutoLanguageSessionState,
request: AutoLanguageRequest,
runtimeGeneration: number,
domain: string | null,
allowedLanguages: string[],
now: number,
): void {
const scopeChanged =
session.runtimeGeneration !== runtimeGeneration || session.domain !== domain;
session.tabId = request.tabId;
session.frameId = request.frameId;
session.suggestionId = request.suggestionId;
session.runtimeGeneration = runtimeGeneration;
session.domain = domain;
session.enabledLanguages = allowedLanguages.slice();
session.lastSeenAt = now;
if (scopeChanged) {
session.pageLanguageHint = null;
session.pageLanguageHintResolved = false;
}
}

reportRuntimeActivity(scope: AutoLanguageSessionLookup): void {
logger.debug("Recording runtime activity", {
tabId: scope.tabId,
Expand Down
71 changes: 34 additions & 37 deletions src/adapters/chrome/background/Migration.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
// Handles migration/version logic for FluentTyper extension
import { SUPPORTED_LANGUAGES } from "@core/domain/lang";
import { SettingsManager } from "@core/application/settingsManager";
import { getSettingStorageKey } from "@core/domain/contracts/settings";
Expand All @@ -7,61 +6,59 @@ import { CoreSettingsRepository } from "@core/application/repositories/CoreSetti
import { SiteProfileRepository } from "@core/application/repositories/SiteProfileRepository";

const LEGACY_REVERT_ON_BACKSPACE_KEY = "revertOnBackspace";
const LAST_VERSION_CUTOFF_STORE = "2023.09.30";
const LAST_VERSION_CUTOFF_LANGUAGE = "2024.04.21";

/**
* Migrates storage and language settings to the latest version.
* @param lastVersion - The previous version string.
*/
export async function migrateToLocalStore(lastVersion?: string): Promise<void> {
const currentVersion = chrome.runtime.getManifest().version;
const migrateStore =
!lastVersion ||
lastVersion.localeCompare("2023.09.30", undefined, {
numeric: true,
sensitivity: "base",
}) <= 0;
const settingsManager = new SettingsManager();

const updateLang =
!lastVersion ||
lastVersion.localeCompare("2024.04.21", undefined, {
numeric: true,
sensitivity: "base",
}) <= 0;

let settingsManager: SettingsManager | null = null;

if (migrateStore) {
if (shouldMigrate(lastVersion, LAST_VERSION_CUTOFF_STORE)) {
chrome.storage.sync.get(null, (result: { [key: string]: unknown }) => {
void chrome.storage.local.set(result);
void chrome.storage.local.set({ lastVersion: currentVersion });
});
}

if (updateLang) {
settingsManager = settingsManager || new SettingsManager();
const langProps: Array<"language" | "fallbackLanguage"> = ["language", "fallbackLanguage"];
for (const langProp of langProps) {
const storageKey = getSettingStorageKey(langProp);
const language = await settingsManager.get(storageKey);
for (const key of Object.keys(SUPPORTED_LANGUAGES)) {
if (typeof language === "string" && key.startsWith(language)) {
await settingsManager.set(storageKey, key);
break;
}
}
}
if (shouldMigrate(lastVersion, LAST_VERSION_CUTOFF_LANGUAGE)) {
await migrateLanguageSettings(settingsManager);
}

settingsManager = settingsManager || new SettingsManager();
if (typeof settingsManager.removeRaw === "function") {
await settingsManager.removeRaw(LEGACY_REVERT_ON_BACKSPACE_KEY);
}

const coreSettings = new CoreSettingsRepository(settingsManager);
const siteProfileRepository = new SiteProfileRepository(settingsManager);
const enabledLanguages = await coreSettings.getEnabledLanguages();
const rawSiteProfiles = await siteProfileRepository.getRawSiteProfiles();
const siteProfiles = resolveSiteProfiles(rawSiteProfiles, enabledLanguages);
await siteProfileRepository.setSiteProfiles(siteProfiles);
await siteProfileRepository.setSiteProfiles(
resolveSiteProfiles(rawSiteProfiles, enabledLanguages),
);

void chrome.storage.local.set({ lastVersion: currentVersion });
}

function shouldMigrate(lastVersion: string | undefined, cutoffVersion: string): boolean {
return (
!lastVersion ||
lastVersion.localeCompare(cutoffVersion, undefined, {
numeric: true,
sensitivity: "base",
}) <= 0
);
}

async function migrateLanguageSettings(settingsManager: SettingsManager): Promise<void> {
const langProps: Array<"language" | "fallbackLanguage"> = ["language", "fallbackLanguage"];
for (const langProp of langProps) {
const storageKey = getSettingStorageKey(langProp);
const language = await settingsManager.get(storageKey);
for (const key of Object.keys(SUPPORTED_LANGUAGES)) {
if (typeof language === "string" && key.startsWith(language)) {
await settingsManager.set(storageKey, key);
break;
}
}
}
}
67 changes: 31 additions & 36 deletions src/adapters/chrome/background/PredictionInputProcessor.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
// Utility for processing prediction input for PresageHandler
import { DEFAULT_SEPARATOR_CHARS_REGEX, LANG_ADDITIONAL_SEPARATOR_REGEX } from "@core/domain/lang";
import {
extractPredictionTokenSuffix,
Expand All @@ -12,12 +11,12 @@ export const PAST_WORDS_COUNT = 5;
export const MIN_WORD_LENGTH_TO_PREDICT = 1;

export class PredictionInputProcessor {
separatorCharRegex: RegExp;
keepPredCharRegex: RegExp;
whiteSpaceRegex: RegExp;
letterRegex: RegExp;
minWordLengthToPredict: number;
autoCapitalize: boolean;
readonly separatorCharRegex: RegExp;
readonly keepPredCharRegex: RegExp;
readonly whiteSpaceRegex: RegExp;
readonly letterRegex: RegExp;
readonly minWordLengthToPredict: number;
readonly autoCapitalize: boolean;

constructor(minWordLengthToPredict = MIN_WORD_LENGTH_TO_PREDICT, autoCapitalize = true) {
this.separatorCharRegex = RegExp(DEFAULT_SEPARATOR_CHARS_REGEX);
Expand All @@ -32,17 +31,17 @@ export class PredictionInputProcessor {
wordArray: string[];
newSentence: boolean;
} {
let newSentence = false;
let wordArray = wordArrayOrig.slice();
const wordArray = wordArrayOrig.slice();
for (let index = wordArray.length - 1; index >= 0; index--) {
const element = wordArray[index];
if (NEW_SENTENCE_CHARS.includes(element) || NEW_SENTENCE_CHARS.includes(element.slice(-1))) {
wordArray = wordArray.splice(index + 1);
newSentence = true;
break;
return {
wordArray: wordArray.slice(index + 1),
newSentence: true,
};
}
}
return { wordArray, newSentence };
return { wordArray, newSentence: false };
}

checkDoPrediction(
Expand All @@ -54,23 +53,15 @@ export class PredictionInputProcessor {
if (numSuggestions <= 0) {
return false;
}
if (!endsWithSpace && isNumber(lastWord)) {
return false;
}
if (endsWithSpace && !predictNextWordAfterSeparatorChar) {
return false;
if (endsWithSpace) {
return predictNextWordAfterSeparatorChar;
}
if (!endsWithSpace && lastWord.length < this.minWordLengthToPredict) {
if (isNumber(lastWord) || lastWord.length < this.minWordLengthToPredict) {
return false;
}
if (
!endsWithSpace &&
(lastWord.match(this.separatorCharRegex) || []).length !==
(lastWord.match(this.keepPredCharRegex) || []).length
) {
return false;
}
return true;
const separatorMatches = lastWord.match(this.separatorCharRegex) || [];
const keepMatches = lastWord.match(this.keepPredCharRegex) || [];
return separatorMatches.length === keepMatches.length;
}

private normalizeAdditionalSeparators(value: string, language: string): string {
Expand Down Expand Up @@ -118,18 +109,18 @@ export class PredictionInputProcessor {
};
}
const endsWithSpace = predictionInput !== predictionInput.trimEnd();
predictionInput = this.normalizeAdditionalSeparators(predictionInput, language);
const normalizedInput = this.normalizeAdditionalSeparators(predictionInput, language);
const currentWordSuffix = this.resolveCurrentWordSuffix(afterCursorTokenSuffix, language);
const predictionInputWithCurrentWord = `${predictionInput}${currentWordSuffix}`;
const predictionInputWithCurrentWord = `${normalizedInput}${currentWordSuffix}`;
const lastWordsArray = predictionInputWithCurrentWord
.split(this.whiteSpaceRegex)
.filter((e) => e.trim())
.splice(-PAST_WORDS_COUNT);
.slice(-PAST_WORDS_COUNT);
const { wordArray, newSentence } = this.removePrevSentence(lastWordsArray);
predictionInput = wordArray.join(" ") + (endsWithSpace ? " " : "");
let lastWord = lastWordsArray.length ? lastWordsArray[lastWordsArray.length - 1] : "";
lastWord =
lastWord
const trimmedPredictionInput = wordArray.join(" ") + (endsWithSpace ? " " : "");
const lastWordRaw = lastWordsArray.length ? lastWordsArray[lastWordsArray.length - 1] : "";
const lastWord =
lastWordRaw
.split(this.keepPredCharRegex)
.filter((e) => e.trim())
.pop() || "";
Expand All @@ -146,7 +137,11 @@ export class PredictionInputProcessor {
numSuggestions,
predictNextWordAfterSeparatorChar,
);
predictionInput = predictionInput.toLowerCase();
return { predictionInput, lastWord, doPrediction, doCapitalize };
return {
predictionInput: trimmedPredictionInput.toLowerCase(),
lastWord,
doPrediction,
doCapitalize,
};
}
}
Loading
Loading