move compatability mode to the advanced settings and map browser language to autofill OCR language if available. TODO: Make the tooltip component to explain what all the settings do

This commit is contained in:
EthanHealy01 2025-07-31 18:15:19 +01:00
parent f033200ba9
commit 8bb7e9056e
7 changed files with 485 additions and 93 deletions

View File

@ -1,5 +1,5 @@
import React, { ReactNode, useState, useMemo } from 'react'; import React, { ReactNode, useState, useMemo } from 'react';
import { Stack, Text, Popover, Box, Checkbox, Group, TextInput, useMantineColorScheme } from '@mantine/core'; import { Stack, Text, Popover, Box, Checkbox, Group, TextInput } from '@mantine/core';
import UnfoldMoreIcon from '@mui/icons-material/UnfoldMore'; import UnfoldMoreIcon from '@mui/icons-material/UnfoldMore';
import SearchIcon from '@mui/icons-material/Search'; import SearchIcon from '@mui/icons-material/Search';
@ -60,7 +60,6 @@ const DropdownListWithFooter: React.FC<DropdownListWithFooterProps> = ({
}) => { }) => {
const [searchTerm, setSearchTerm] = useState(''); const [searchTerm, setSearchTerm] = useState('');
const { colorScheme } = useMantineColorScheme();
const isMultiValue = Array.isArray(value); const isMultiValue = Array.isArray(value);
const selectedValues = isMultiValue ? value : (value ? [value] : []); const selectedValues = isMultiValue ? value : (value ? [value] : []);
@ -119,14 +118,10 @@ const DropdownListWithFooter: React.FC<DropdownListWithFooterProps> = ({
<Popover.Target> <Popover.Target>
<Box <Box
style={{ style={{
border: colorScheme === 'dark' border: 'light-dark(1px solid var(--mantine-color-gray-3), 1px solid var(--mantine-color-dark-4))',
? '1px solid var(--mantine-color-dark-4)'
: '1px solid var(--mantine-color-gray-3)',
borderRadius: 'var(--mantine-radius-sm)', borderRadius: 'var(--mantine-radius-sm)',
padding: '8px 12px', padding: '8px 12px',
backgroundColor: colorScheme === 'dark' backgroundColor: 'light-dark(var(--mantine-color-white), var(--mantine-color-dark-6))',
? 'var(--mantine-color-dark-6)'
: 'var(--mantine-color-white)',
opacity: disabled ? 0.6 : 1, opacity: disabled ? 0.6 : 1,
cursor: disabled ? 'not-allowed' : 'pointer', cursor: disabled ? 'not-allowed' : 'pointer',
minHeight: '36px', minHeight: '36px',
@ -140,9 +135,7 @@ const DropdownListWithFooter: React.FC<DropdownListWithFooterProps> = ({
</Text> </Text>
<UnfoldMoreIcon style={{ <UnfoldMoreIcon style={{
fontSize: '1rem', fontSize: '1rem',
color: colorScheme === 'dark' color: 'light-dark(var(--mantine-color-gray-5), var(--mantine-color-dark-2))'
? 'var(--mantine-color-dark-2)'
: 'var(--mantine-color-gray-5)'
}} /> }} />
</Box> </Box>
</Popover.Target> </Popover.Target>
@ -151,9 +144,7 @@ const DropdownListWithFooter: React.FC<DropdownListWithFooterProps> = ({
<Stack gap="xs"> <Stack gap="xs">
{header && ( {header && (
<Box style={{ <Box style={{
borderBottom: colorScheme === 'dark' borderBottom: 'light-dark(1px solid var(--mantine-color-gray-2), 1px solid var(--mantine-color-dark-4))',
? '1px solid var(--mantine-color-dark-4)'
: '1px solid var(--mantine-color-gray-2)',
paddingBottom: '8px' paddingBottom: '8px'
}}> }}>
{header} {header}
@ -162,9 +153,7 @@ const DropdownListWithFooter: React.FC<DropdownListWithFooterProps> = ({
{searchable && ( {searchable && (
<Box style={{ <Box style={{
borderBottom: colorScheme === 'dark' borderBottom: 'light-dark(1px solid var(--mantine-color-gray-2), 1px solid var(--mantine-color-dark-4))',
? '1px solid var(--mantine-color-dark-4)'
: '1px solid var(--mantine-color-gray-2)',
paddingBottom: '8px' paddingBottom: '8px'
}}> }}>
<TextInput <TextInput
@ -201,9 +190,7 @@ const DropdownListWithFooter: React.FC<DropdownListWithFooterProps> = ({
}} }}
onMouseEnter={(e) => { onMouseEnter={(e) => {
if (!item.disabled) { if (!item.disabled) {
e.currentTarget.style.backgroundColor = colorScheme === 'dark' e.currentTarget.style.backgroundColor = 'light-dark(var(--mantine-color-gray-0), var(--mantine-color-dark-5))';
? 'var(--mantine-color-dark-5)'
: 'var(--mantine-color-gray-0)';
} }
}} }}
onMouseLeave={(e) => { onMouseLeave={(e) => {
@ -234,9 +221,7 @@ const DropdownListWithFooter: React.FC<DropdownListWithFooterProps> = ({
{footer && ( {footer && (
<Box style={{ <Box style={{
borderTop: colorScheme === 'dark' borderTop: 'light-dark(1px solid var(--mantine-color-gray-2), 1px solid var(--mantine-color-dark-4))',
? '1px solid var(--mantine-color-dark-4)'
: '1px solid var(--mantine-color-gray-2)',
paddingTop: '8px' paddingTop: '8px'
}}> }}>
{footer} {footer}

View File

@ -1,5 +1,5 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import { Menu, Button, ScrollArea, useMantineTheme, useMantineColorScheme } from '@mantine/core'; import { Menu, Button, ScrollArea } from '@mantine/core';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { supportedLanguages } from '../../i18n'; import { supportedLanguages } from '../../i18n';
import LanguageIcon from '@mui/icons-material/Language'; import LanguageIcon from '@mui/icons-material/Language';
@ -7,8 +7,6 @@ import styles from './LanguageSelector.module.css';
const LanguageSelector = () => { const LanguageSelector = () => {
const { i18n } = useTranslation(); const { i18n } = useTranslation();
const theme = useMantineTheme();
const { colorScheme } = useMantineColorScheme();
const [opened, setOpened] = useState(false); const [opened, setOpened] = useState(false);
const [animationTriggered, setAnimationTriggered] = useState(false); const [animationTriggered, setAnimationTriggered] = useState(false);
const [isChanging, setIsChanging] = useState(false); const [isChanging, setIsChanging] = useState(false);
@ -102,10 +100,10 @@ const LanguageSelector = () => {
styles={{ styles={{
root: { root: {
border: 'none', border: 'none',
color: colorScheme === 'dark' ? theme.colors.gray[1] : theme.colors.gray[7], color: 'light-dark(var(--mantine-color-gray-7), var(--mantine-color-gray-1))',
transition: 'background-color 0.2s cubic-bezier(0.25, 0.46, 0.45, 0.94)', transition: 'background-color 0.2s cubic-bezier(0.25, 0.46, 0.45, 0.94)',
'&:hover': { '&:hover': {
backgroundColor: colorScheme === 'dark' ? theme.colors.dark[5] : theme.colors.gray[1], backgroundColor: 'light-dark(var(--mantine-color-gray-1), var(--mantine-color-dark-5))',
} }
}, },
label: { label: {
@ -125,8 +123,8 @@ const LanguageSelector = () => {
padding: '12px', padding: '12px',
borderRadius: '8px', borderRadius: '8px',
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.1)', boxShadow: '0 4px 12px rgba(0, 0, 0, 0.1)',
backgroundColor: colorScheme === 'dark' ? theme.colors.dark[6] : theme.white, backgroundColor: 'light-dark(var(--mantine-color-white), var(--mantine-color-dark-6))',
border: colorScheme === 'dark' ? `1px solid ${theme.colors.dark[4]}` : `1px solid ${theme.colors.gray[3]}`, border: 'light-dark(1px solid var(--mantine-color-gray-3), 1px solid var(--mantine-color-dark-4))',
}} }}
> >
<ScrollArea h={190} type="scroll"> <ScrollArea h={190} type="scroll">
@ -146,6 +144,7 @@ const LanguageSelector = () => {
size="sm" size="sm"
fullWidth fullWidth
onClick={(event) => handleLanguageChange(option.value, event)} onClick={(event) => handleLanguageChange(option.value, event)}
data-selected={option.value === i18n.language}
styles={{ styles={{
root: { root: {
borderRadius: '4px', borderRadius: '4px',
@ -154,21 +153,17 @@ const LanguageSelector = () => {
justifyContent: 'flex-start', justifyContent: 'flex-start',
position: 'relative', position: 'relative',
overflow: 'hidden', overflow: 'hidden',
backgroundColor: option.value === i18n.language ? ( backgroundColor: option.value === i18n.language
colorScheme === 'dark' ? theme.colors.blue[8] : theme.colors.blue[1] ? 'light-dark(var(--mantine-color-blue-1), var(--mantine-color-blue-8))'
) : 'transparent', : 'transparent',
color: option.value === i18n.language ? ( color: option.value === i18n.language
colorScheme === 'dark' ? theme.white : theme.colors.blue[9] ? 'light-dark(var(--mantine-color-blue-9), var(--mantine-color-white))'
) : ( : 'light-dark(var(--mantine-color-gray-7), var(--mantine-color-white))',
colorScheme === 'dark' ? theme.white : theme.colors.gray[7]
),
transition: 'all 0.2s cubic-bezier(0.25, 0.46, 0.45, 0.94)', transition: 'all 0.2s cubic-bezier(0.25, 0.46, 0.45, 0.94)',
'&:hover': { '&:hover': {
backgroundColor: option.value === i18n.language ? ( backgroundColor: option.value === i18n.language
colorScheme === 'dark' ? theme.colors.blue[7] : theme.colors.blue[2] ? 'light-dark(var(--mantine-color-blue-2), var(--mantine-color-blue-7))'
) : ( : 'light-dark(var(--mantine-color-gray-1), var(--mantine-color-dark-5))',
colorScheme === 'dark' ? theme.colors.dark[5] : theme.colors.gray[1]
),
transform: 'translateY(-1px)', transform: 'translateY(-1px)',
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.1)', boxShadow: '0 2px 8px rgba(0, 0, 0, 0.1)',
} }
@ -198,7 +193,7 @@ const LanguageSelector = () => {
width: 0, width: 0,
height: 0, height: 0,
borderRadius: '50%', borderRadius: '50%',
backgroundColor: theme.colors.blue[4], backgroundColor: 'var(--mantine-color-blue-4)',
opacity: 0.6, opacity: 0.6,
transform: 'translate(-50%, -50%)', transform: 'translate(-50%, -50%)',
animation: 'ripple-expand 0.6s cubic-bezier(0.25, 0.46, 0.45, 0.94)', animation: 'ripple-expand 0.6s cubic-bezier(0.25, 0.46, 0.45, 0.94)',

View File

@ -1,79 +1,80 @@
import React from 'react'; import React from 'react';
import { Stack, Text, Divider, Switch, Group, Checkbox } from '@mantine/core'; import { Stack, Text, Checkbox } from '@mantine/core';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { OCRParameters } from './OCRSettings'; import { OCRParameters } from './OCRSettings';
export interface AdvancedOCRParameters { export interface AdvancedOCRParameters {
ocrRenderType: string;
advancedOptions: string[]; advancedOptions: string[];
} }
interface AdvancedOption {
value: string;
label: string;
isSpecial: boolean;
}
interface AdvancedOCRSettingsProps { interface AdvancedOCRSettingsProps {
ocrRenderType: string;
advancedOptions: string[]; advancedOptions: string[];
ocrRenderType?: string;
onParameterChange: (key: keyof OCRParameters, value: any) => void; onParameterChange: (key: keyof OCRParameters, value: any) => void;
disabled?: boolean; disabled?: boolean;
} }
const AdvancedOCRSettings: React.FC<AdvancedOCRSettingsProps> = ({ const AdvancedOCRSettings: React.FC<AdvancedOCRSettingsProps> = ({
ocrRenderType,
advancedOptions, advancedOptions,
ocrRenderType = 'hocr',
onParameterChange, onParameterChange,
disabled = false disabled = false
}) => { }) => {
const { t } = useTranslation(); const { t } = useTranslation();
// Define the advanced options available // Define the advanced options available
const advancedOptionsData = [ const advancedOptionsData: AdvancedOption[] = [
{ value: 'sidecar', label: t('ocr.settings.advancedOptions.sidecar', 'Create a text file') }, { value: 'compatibilityMode', label: t('ocr.settings.compatibilityMode.label', 'Compatibility Mode'), isSpecial: true },
{ value: 'deskew', label: t('ocr.settings.advancedOptions.deskew', 'Deskew pages') }, { value: 'sidecar', label: t('ocr.settings.advancedOptions.sidecar', 'Create a text file'), isSpecial: false },
{ value: 'clean', label: t('ocr.settings.advancedOptions.clean', 'Clean input file') }, { value: 'deskew', label: t('ocr.settings.advancedOptions.deskew', 'Deskew pages'), isSpecial: false },
{ value: 'cleanFinal', label: t('ocr.settings.advancedOptions.cleanFinal', 'Clean final output') }, { value: 'clean', label: t('ocr.settings.advancedOptions.clean', 'Clean input file'), isSpecial: false },
{ value: 'cleanFinal', label: t('ocr.settings.advancedOptions.cleanFinal', 'Clean final output'), isSpecial: false },
]; ];
// Handle individual checkbox changes // Handle individual checkbox changes
const handleCheckboxChange = (optionValue: string, checked: boolean) => { const handleCheckboxChange = (optionValue: string, checked: boolean) => {
const newOptions = checked const option = advancedOptionsData.find(opt => opt.value === optionValue);
? [...advancedOptions, optionValue]
: advancedOptions.filter(option => option !== optionValue); if (option?.isSpecial) {
onParameterChange('additionalOptions', newOptions); // Handle special options (like compatibility mode) differently
if (optionValue === 'compatibilityMode') {
onParameterChange('ocrRenderType', checked ? 'sandwich' : 'hocr');
}
} else {
// Handle regular advanced options
const newOptions = checked
? [...advancedOptions, optionValue]
: advancedOptions.filter(option => option !== optionValue);
onParameterChange('additionalOptions', newOptions);
}
};
// Check if a special option is selected
const isSpecialOptionSelected = (optionValue: string) => {
if (optionValue === 'compatibilityMode') {
return ocrRenderType === 'sandwich';
}
return false;
}; };
return ( return (
<Stack gap="md"> <Stack gap="md">
<div>
<Text size="sm" fw={500} mb="sm" mt="md">
{t('ocr.settings.output.label', 'Output Render Type ')}
</Text>
<Group justify="space-between" align="center" gap="xs" wrap="nowrap">
<Text size="xs" style={{ flex: '0 1 auto', lineHeight: 1.3, textAlign: 'left' }}>
{t('ocr.settings.output.hocr', 'HOCR (Auto)')}
</Text>
<Switch
checked={ocrRenderType === 'sandwich'}
onChange={(event) => onParameterChange('ocrRenderType', event.currentTarget.checked ? 'sandwich' : 'hocr')}
disabled={disabled}
size="sm"
style={{ flexShrink: 0 }}
/>
<Text size="xs" style={{ flex: '0 1 auto', lineHeight: 1.3, textAlign: 'right' }}>
{t('ocr.settings.output.sandwich', 'Searchable PDF')}
</Text>
</Group>
</div>
<Divider />
<div> <div>
<Text size="sm" fw={500} mb="md"> <Text size="sm" fw={500} mb="md">
{t('ocr.settings.advancedOptions.label', 'Processing Options')} {t('ocr.settings.advancedOptions.label', 'Processing Options')}
</Text> </Text>
<Stack gap="sm"> <Stack gap="sm">
{advancedOptionsData.map((option) => ( {advancedOptionsData.map((option) => (
<Checkbox <Checkbox
key={option.value} key={option.value}
checked={advancedOptions.includes(option.value)} checked={option.isSpecial ? isSpecialOptionSelected(option.value) : advancedOptions.includes(option.value)}
onChange={(event) => handleCheckboxChange(option.value, event.currentTarget.checked)} onChange={(event) => handleCheckboxChange(option.value, event.currentTarget.checked)}
label={option.label} label={option.label}
disabled={disabled} disabled={disabled}

View File

@ -1,7 +1,8 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import { Text, Loader, useMantineColorScheme } from '@mantine/core'; import { Text, Loader } from '@mantine/core';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { tempOcrLanguages } from '../../../utils/tempOcrLanguages'; import { tempOcrLanguages } from '../../../utils/tempOcrLanguages';
import { getAutoOcrLanguage } from '../../../utils/languageMapping';
import DropdownListWithFooter, { DropdownItem } from '../../shared/DropdownListWithFooter'; import DropdownListWithFooter, { DropdownItem } from '../../shared/DropdownListWithFooter';
export interface LanguageOption { export interface LanguageOption {
@ -16,6 +17,7 @@ export interface LanguagePickerProps {
disabled?: boolean; disabled?: boolean;
label?: string; label?: string;
languagesEndpoint?: string; languagesEndpoint?: string;
autoFillFromBrowserLanguage?: boolean;
} }
const LanguagePicker: React.FC<LanguagePickerProps> = ({ const LanguagePicker: React.FC<LanguagePickerProps> = ({
@ -24,12 +26,13 @@ const LanguagePicker: React.FC<LanguagePickerProps> = ({
placeholder = 'Select languages', placeholder = 'Select languages',
disabled = false, disabled = false,
label, label,
languagesEndpoint = '/api/v1/ui-data/ocr-pdf' languagesEndpoint = '/api/v1/ui-data/ocr-pdf',
autoFillFromBrowserLanguage = true,
}) => { }) => {
const { t } = useTranslation(); const { t, i18n } = useTranslation();
const { colorScheme } = useMantineColorScheme();
const [availableLanguages, setAvailableLanguages] = useState<DropdownItem[]>([]); const [availableLanguages, setAvailableLanguages] = useState<DropdownItem[]>([]);
const [isLoadingLanguages, setIsLoadingLanguages] = useState(true); const [isLoadingLanguages, setIsLoadingLanguages] = useState(true);
const [hasAutoFilled, setHasAutoFilled] = useState(false);
useEffect(() => { useEffect(() => {
// Fetch available languages from backend // Fetch available languages from backend
@ -76,6 +79,29 @@ const LanguagePicker: React.FC<LanguagePickerProps> = ({
fetchLanguages(); fetchLanguages();
}, [languagesEndpoint]); }, [languagesEndpoint]);
// Auto-fill OCR language based on browser language when languages are loaded
useEffect(() => {
const shouldAutoFillLanguage = autoFillFromBrowserLanguage && !isLoadingLanguages && availableLanguages.length > 0 && !hasAutoFilled && value.length === 0;
if (shouldAutoFillLanguage) {
// Use the comprehensive language mapping from languageMapping.ts
const suggestedOcrLanguages = getAutoOcrLanguage(i18n.language);
if (suggestedOcrLanguages.length > 0) {
// Find the first suggested language that's available in the backend
const matchingLanguage = availableLanguages.find(lang =>
suggestedOcrLanguages.includes(lang.value)
);
if (matchingLanguage) {
onChange([matchingLanguage.value]);
}
}
setHasAutoFilled(true);
}
}, [autoFillFromBrowserLanguage, isLoadingLanguages, availableLanguages, hasAutoFilled, value.length, i18n.language, onChange]);
if (isLoadingLanguages) { if (isLoadingLanguages) {
return ( return (
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}> <div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
@ -87,22 +113,23 @@ const LanguagePicker: React.FC<LanguagePickerProps> = ({
const footer = ( const footer = (
<> <>
<Text size="xs" c="dimmed" mb={4}> <div className="flex flex-col items-center gap-1 text-center">
<Text size="xs" c="dimmed" className="text-center">
{t('ocr.languagePicker.additionalLanguages', 'Looking for additional languages?')} {t('ocr.languagePicker.additionalLanguages', 'Looking for additional languages?')}
</Text> </Text>
<Text <Text
size="xs" size="xs"
style={{ style={{
color: colorScheme === 'dark' color: '#3b82f6',
? 'var(--mantine-color-blue-4)'
: 'var(--mantine-color-blue-6)',
cursor: 'pointer', cursor: 'pointer',
textDecoration: 'underline' textDecoration: 'underline',
textAlign: 'center'
}} }}
onClick={() => window.open('https://docs.stirlingpdf.com/Advanced%20Configuration/OCR', '_blank')} onClick={() => window.open('https://docs.stirlingpdf.com/Advanced%20Configuration/OCR', '_blank')}
> >
{t('ocr.languagePicker.viewSetupGuide', 'View setup guide →')} {t('ocr.languagePicker.viewSetupGuide', 'View setup guide →')}
</Text> </Text>
</div>
</> </>
); );
@ -117,6 +144,7 @@ const LanguagePicker: React.FC<LanguagePickerProps> = ({
footer={footer} footer={footer}
multiSelect={true} multiSelect={true}
maxHeight={300} maxHeight={300}
searchable={true}
/> />
); );
}; };

View File

@ -9,7 +9,7 @@ export interface OCRParametersHook {
} }
const defaultParameters: OCRParameters = { const defaultParameters: OCRParameters = {
languages: ['eng'], languages: [],
ocrType: 'skip-text', ocrType: 'skip-text',
ocrRenderType: 'hocr', ocrRenderType: 'hocr',
additionalOptions: [], additionalOptions: [],

View File

@ -150,8 +150,8 @@ const OCR = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
completedMessage={hasFiles && hasResults && expandedStep !== 'advanced' ? "OCR processing completed" : undefined} completedMessage={hasFiles && hasResults && expandedStep !== 'advanced' ? "OCR processing completed" : undefined}
> >
<AdvancedOCRSettings <AdvancedOCRSettings
ocrRenderType={ocrParams.parameters.ocrRenderType}
advancedOptions={ocrParams.parameters.additionalOptions} advancedOptions={ocrParams.parameters.additionalOptions}
ocrRenderType={ocrParams.parameters.ocrRenderType}
onParameterChange={ocrParams.updateParameter} onParameterChange={ocrParams.updateParameter}
disabled={endpointLoading} disabled={endpointLoading}
/> />

View File

@ -0,0 +1,383 @@
// Mapping from browser language codes to OCR language codes
// Handles exact matches and similar language fallbacks
interface LanguageMapping {
[browserCode: string]: string;
}
// Primary mapping from browser language codes to OCR language codes
const browserToOcrMapping: LanguageMapping = {
// English variants
'en': 'eng',
'en-US': 'eng',
'en-GB': 'eng',
'en-AU': 'eng',
'en-CA': 'eng',
'en-IE': 'eng',
'en-NZ': 'eng',
'en-ZA': 'eng',
// Spanish variants
'es': 'spa',
'es-ES': 'spa',
'es-MX': 'spa',
'es-AR': 'spa',
'es-CO': 'spa',
'es-CL': 'spa',
'es-PE': 'spa',
'es-VE': 'spa',
// French variants
'fr': 'fra',
'fr-FR': 'fra',
'fr-CA': 'fra',
'fr-BE': 'fra',
'fr-CH': 'fra',
// German variants
'de': 'deu',
'de-DE': 'deu',
'de-AT': 'deu',
'de-CH': 'deu',
// Portuguese variants
'pt': 'por',
'pt-PT': 'por',
'pt-BR': 'por',
// Italian variants
'it': 'ita',
'it-IT': 'ita',
'it-CH': 'ita',
// Chinese variants
'zh': 'chi_sim',
'zh-CN': 'chi_sim',
'zh-Hans': 'chi_sim',
'zh-TW': 'chi_tra',
'zh-HK': 'chi_tra',
'zh-Hant': 'chi_tra',
'zh-BO': 'bod',
// Japanese
'ja': 'jpn',
'ja-JP': 'jpn',
// Korean
'ko': 'kor',
'ko-KR': 'kor',
// Russian variants
'ru': 'rus',
'ru-RU': 'rus',
// Arabic variants
'ar': 'ara',
'ar-SA': 'ara',
'ar-EG': 'ara',
'ar-AE': 'ara',
'ar-MA': 'ara',
// Dutch variants
'nl': 'nld',
'nl-NL': 'nld',
'nl-BE': 'nld',
// Polish
'pl': 'pol',
'pl-PL': 'pol',
// Czech
'cs': 'ces',
'cs-CZ': 'ces',
// Slovak
'sk': 'slk',
'sk-SK': 'slk',
// Hungarian
'hu': 'hun',
'hu-HU': 'hun',
// Romanian
'ro': 'ron',
'ro-RO': 'ron',
// Bulgarian
'bg': 'bul',
'bg-BG': 'bul',
// Croatian
'hr': 'hrv',
'hr-HR': 'hrv',
// Serbian
'sr': 'srp',
'sr-RS': 'srp',
'sr-Latn': 'srp_latn',
// Slovenian
'sl': 'slv',
'sl-SI': 'slv',
// Estonian
'et': 'est',
'et-EE': 'est',
// Latvian
'lv': 'lav',
'lv-LV': 'lav',
// Lithuanian
'lt': 'lit',
'lt-LT': 'lit',
// Finnish
'fi': 'fin',
'fi-FI': 'fin',
// Swedish
'sv': 'swe',
'sv-SE': 'swe',
// Norwegian
'no': 'nor',
'nb': 'nor',
'nn': 'nor',
'no-NO': 'nor',
'nb-NO': 'nor',
'nn-NO': 'nor',
// Danish
'da': 'dan',
'da-DK': 'dan',
// Icelandic
'is': 'isl',
'is-IS': 'isl',
// Greek
'el': 'ell',
'el-GR': 'ell',
// Turkish
'tr': 'tur',
'tr-TR': 'tur',
// Hebrew
'he': 'heb',
'he-IL': 'heb',
// Hindi
'hi': 'hin',
'hi-IN': 'hin',
// Thai
'th': 'tha',
'th-TH': 'tha',
// Vietnamese
'vi': 'vie',
'vi-VN': 'vie',
// Indonesian
'id': 'ind',
'id-ID': 'ind',
// Malay
'ms': 'msa',
'ms-MY': 'msa',
// Filipino/Tagalog
'fil': 'fil',
'tl': 'tgl',
// Ukrainian
'uk': 'ukr',
'uk-UA': 'ukr',
// Belarusian
'be': 'bel',
'be-BY': 'bel',
// Kazakh
'kk': 'kaz',
'kk-KZ': 'kaz',
// Uzbek
'uz': 'uzb',
'uz-UZ': 'uzb',
// Georgian
'ka': 'kat',
'ka-GE': 'kat',
// Armenian
'hy': 'hye',
'hy-AM': 'hye',
// Azerbaijani
'az': 'aze',
'az-AZ': 'aze',
// Persian/Farsi
'fa': 'fas',
'fa-IR': 'fas',
// Urdu
'ur': 'urd',
'ur-PK': 'urd',
// Bengali
'bn': 'ben',
'bn-BD': 'ben',
'bn-IN': 'ben',
// Tamil
'ta': 'tam',
'ta-IN': 'tam',
'ta-LK': 'tam',
// Telugu
'te': 'tel',
'te-IN': 'tel',
// Kannada
'kn': 'kan',
'kn-IN': 'kan',
// Malayalam
'ml': 'mal',
'ml-IN': 'mal',
// Gujarati
'gu': 'guj',
'gu-IN': 'guj',
// Marathi
'mr': 'mar',
'mr-IN': 'mar',
// Punjabi
'pa': 'pan',
'pa-IN': 'pan',
// Nepali
'ne': 'nep',
'ne-NP': 'nep',
// Sinhala
'si': 'sin',
'si-LK': 'sin',
// Burmese
'my': 'mya',
'my-MM': 'mya',
// Khmer
'km': 'khm',
'km-KH': 'khm',
// Lao
'lo': 'lao',
'lo-LA': 'lao',
// Mongolian
'mn': 'mon',
'mn-MN': 'mon',
// Welsh
'cy': 'cym',
'cy-GB': 'cym',
// Irish
'ga': 'gle',
'ga-IE': 'gle',
// Scottish Gaelic
'gd': 'gla',
'gd-GB': 'gla',
// Basque
'eu': 'eus',
'eu-ES': 'eus',
// Catalan
'ca': 'cat',
'ca-ES': 'cat',
// Galician
'gl': 'glg',
'gl-ES': 'glg',
// Macedonian
'mk': 'mkd',
'mk-MK': 'mkd',
// Albanian
'sq': 'sqi',
'sq-AL': 'sqi',
// Maltese
'mt': 'mlt',
'mt-MT': 'mlt',
// Afrikaans
'af': 'afr',
'af-ZA': 'afr',
// Swahili
'sw': 'swa',
'sw-KE': 'swa',
'sw-TZ': 'swa',
};
/**
* Maps a browser language code to an OCR language code
* Handles exact matches and similar language fallbacks
*
* @param browserLanguage - The browser language code (e.g., 'en-GB', 'fr-FR')
* @returns OCR language code if found, null if no match
*/
export function mapBrowserLanguageToOcr(browserLanguage: string): string | null {
if (!browserLanguage) return null;
// Normalize the input
const normalizedInput = browserLanguage.toLowerCase().replace('_', '-');
// Try exact match first
const exactMatch = browserToOcrMapping[normalizedInput];
if (exactMatch) return exactMatch;
// Try with different casing variations
const variations = [
browserLanguage,
browserLanguage.toLowerCase(),
browserLanguage.toUpperCase(),
normalizedInput,
];
for (const variant of variations) {
const match = browserToOcrMapping[variant];
if (match) return match;
}
// Try base language code (e.g., 'en' from 'en-GB')
const baseLanguage = normalizedInput.split('-')[0];
const baseMatch = browserToOcrMapping[baseLanguage];
if (baseMatch) return baseMatch;
// No match found
return null;
}
/**
* Gets the OCR language code for the current browser language
*
* @param currentLanguage - Current i18n language
* @returns OCR language code array (empty if no match)
*/
export function getAutoOcrLanguage(currentLanguage: string): string[] {
const ocrLanguage = mapBrowserLanguageToOcr(currentLanguage);
return ocrLanguage ? [ocrLanguage] : [];
}