mirror of
https://github.com/zadam/trilium.git
synced 2026-07-07 17:23:54 +02:00
feat(ocr): integrate with content language
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { t } from "../../../services/i18n";
|
||||
import FormTextBox, { FormTextBoxWithUnit } from "../../react/FormTextBox";
|
||||
import { FormTextBoxWithUnit } from "../../react/FormTextBox";
|
||||
import FormToggle from "../../react/FormToggle";
|
||||
import { useTriliumOption, useTriliumOptionBool } from "../../react/hooks";
|
||||
import OptionsRow from "./components/OptionsRow";
|
||||
@@ -62,7 +62,6 @@ function ImageSettings() {
|
||||
function OcrSettings() {
|
||||
const [ ocrEnabled, setOcrEnabled ] = useTriliumOptionBool("ocrEnabled");
|
||||
const [ ocrAutoProcess, setOcrAutoProcess ] = useTriliumOptionBool("ocrAutoProcessImages");
|
||||
const [ ocrLanguage, setOcrLanguage ] = useTriliumOption("ocrLanguage");
|
||||
const [ ocrMinConfidence, setOcrMinConfidence ] = useTriliumOption("ocrMinConfidence");
|
||||
|
||||
return (
|
||||
@@ -84,14 +83,6 @@ function OcrSettings() {
|
||||
/>
|
||||
</OptionsRow>
|
||||
|
||||
<OptionsRow name="ocr-language" label={t("images.ocr_language")} description={t("images.ocr_language_description")}>
|
||||
<FormTextBox
|
||||
disabled={!ocrEnabled}
|
||||
currentValue={ocrLanguage}
|
||||
onChange={setOcrLanguage}
|
||||
/>
|
||||
</OptionsRow>
|
||||
|
||||
<OptionsRow name="ocr-min-confidence" label={t("images.ocr_min_confidence")} description={t("images.ocr_confidence_description")}>
|
||||
<FormTextBoxWithUnit
|
||||
type="number" min="0" max="1" step="0.05"
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
import { getTesseractCode } from '@triliumnext/commons';
|
||||
import Tesseract from 'tesseract.js';
|
||||
import log from '../log.js';
|
||||
import sql from '../sql.js';
|
||||
|
||||
import becca from '../../becca/becca.js';
|
||||
import log from '../log.js';
|
||||
import options from '../options.js';
|
||||
import sql from '../sql.js';
|
||||
import { FileProcessor } from './processors/file_processor.js';
|
||||
import { ImageProcessor } from './processors/image_processor.js';
|
||||
import { OfficeProcessor } from './processors/office_processor.js';
|
||||
import { PDFProcessor } from './processors/pdf_processor.js';
|
||||
import { TIFFProcessor } from './processors/tiff_processor.js';
|
||||
import { OfficeProcessor } from './processors/office_processor.js';
|
||||
import { FileProcessor } from './processors/file_processor.js';
|
||||
|
||||
export interface OCRResult {
|
||||
text: string;
|
||||
@@ -59,6 +61,69 @@ class OCRService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves the Tesseract language code(s) for OCR processing.
|
||||
*
|
||||
* Priority:
|
||||
* 1. Explicitly passed `language` option (e.g. from API call)
|
||||
* 2. The note's `language` label (mapped via {@link getTesseractCode})
|
||||
* 3. All enabled content languages joined with `+`
|
||||
* 4. The UI locale
|
||||
* 5. Fallback to `eng`
|
||||
*/
|
||||
resolveOcrLanguage(noteId?: string, explicitLanguage?: string): string {
|
||||
// 1. Explicit language from caller
|
||||
if (explicitLanguage) {
|
||||
return explicitLanguage;
|
||||
}
|
||||
|
||||
// 2. Note's language label
|
||||
if (noteId) {
|
||||
const note = becca.getNote(noteId);
|
||||
const noteLanguage = note?.getLabelValue("language");
|
||||
if (noteLanguage) {
|
||||
const code = getTesseractCode(noteLanguage);
|
||||
if (code) {
|
||||
return code;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3. All enabled content languages
|
||||
try {
|
||||
const languagesJson = options.getOption("languages");
|
||||
const enabledLanguages = JSON.parse(languagesJson || "[]") as string[];
|
||||
if (enabledLanguages.length > 0) {
|
||||
const codes = enabledLanguages
|
||||
.map((id) => getTesseractCode(id))
|
||||
.filter((code): code is string => code !== null);
|
||||
// Deduplicate (e.g. en + en-GB both map to eng)
|
||||
const unique = [...new Set(codes)];
|
||||
if (unique.length > 0) {
|
||||
return unique.join("+");
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Fall through
|
||||
}
|
||||
|
||||
// 4. UI locale
|
||||
try {
|
||||
const uiLocale = options.getOption("locale");
|
||||
if (uiLocale) {
|
||||
const code = getTesseractCode(uiLocale);
|
||||
if (code) {
|
||||
return code;
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Fall through
|
||||
}
|
||||
|
||||
// 5. Fallback
|
||||
return "eng";
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a MIME type is supported for OCR
|
||||
*/
|
||||
@@ -84,7 +149,7 @@ class OCRService {
|
||||
*/
|
||||
async extractTextFromFile(fileBuffer: Buffer, mimeType: string, options: OCRProcessingOptions = {}): Promise<OCRResult> {
|
||||
try {
|
||||
log.info(`Starting OCR text extraction for MIME type: ${mimeType}`);
|
||||
log.info(`Starting OCR text extraction for MIME type: ${mimeType} with language: ${options.language || "auto-detect"}`);
|
||||
this.isProcessing = true;
|
||||
|
||||
// Find appropriate processor
|
||||
@@ -147,7 +212,8 @@ class OCRService {
|
||||
throw new Error(`Cannot get image content for note ${noteId}`);
|
||||
}
|
||||
|
||||
const ocrResult = await this.extractTextFromFile(content, note.mime, options);
|
||||
const language = this.resolveOcrLanguage(noteId, options.language);
|
||||
const ocrResult = await this.extractTextFromFile(content, note.mime, { ...options, language });
|
||||
|
||||
// Store OCR result in blob
|
||||
await this.storeOCRResult(note.blobId, ocrResult);
|
||||
@@ -200,7 +266,8 @@ class OCRService {
|
||||
throw new Error(`Cannot get image content for attachment ${attachmentId}`);
|
||||
}
|
||||
|
||||
const ocrResult = await this.extractTextFromFile(content, attachment.mime, options);
|
||||
const language = this.resolveOcrLanguage(attachment.ownerId, options.language);
|
||||
const ocrResult = await this.extractTextFromFile(content, attachment.mime, { ...options, language });
|
||||
|
||||
// Store OCR result in blob
|
||||
await this.storeOCRResult(attachment.blobId, ocrResult);
|
||||
|
||||
@@ -43,7 +43,7 @@ export class ImageProcessor extends FileProcessor {
|
||||
|
||||
// Set language if specified and different from current
|
||||
// Support multi-language format like 'ron+eng'
|
||||
const language = options.language || this.getDefaultOCRLanguage();
|
||||
const language = options.language || "eng";
|
||||
|
||||
// Validate language format
|
||||
if (!this.isValidLanguageFormat(language)) {
|
||||
@@ -72,7 +72,7 @@ export class ImageProcessor extends FileProcessor {
|
||||
text: filteredText,
|
||||
confidence: overallConfidence,
|
||||
extractedAt: new Date().toISOString(),
|
||||
language: options.language || this.getDefaultOCRLanguage(),
|
||||
language: options.language || "eng",
|
||||
pageCount: 1
|
||||
};
|
||||
|
||||
@@ -105,7 +105,7 @@ export class ImageProcessor extends FileProcessor {
|
||||
log.info(`Using worker path: ${workerPath}`);
|
||||
log.info(`Using core path: ${corePath}`);
|
||||
|
||||
this.worker = await Tesseract.createWorker(this.getDefaultOCRLanguage(), 1, {
|
||||
this.worker = await Tesseract.createWorker("eng", 1, {
|
||||
workerPath,
|
||||
corePath,
|
||||
logger: (m: { status: string; progress: number }) => {
|
||||
@@ -131,21 +131,7 @@ export class ImageProcessor extends FileProcessor {
|
||||
log.info('Image OCR processor cleaned up');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get default OCR language from options
|
||||
*/
|
||||
private getDefaultOCRLanguage(): string {
|
||||
try {
|
||||
const ocrLanguage = options.getOption('ocrLanguage');
|
||||
if (!ocrLanguage) {
|
||||
throw new Error('OCR language not configured in user settings');
|
||||
}
|
||||
return ocrLanguage;
|
||||
} catch (error) {
|
||||
log.error(`Failed to get default OCR language: ${error}`);
|
||||
throw new Error('OCR language must be configured in settings before processing');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Filter text based on minimum confidence threshold
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import * as officeParser from 'officeparser';
|
||||
|
||||
import log from '../../log.js';
|
||||
import options from '../../options.js';
|
||||
import { OCRProcessingOptions,OCRResult } from '../ocr_service.js';
|
||||
import { FileProcessor } from './file_processor.js';
|
||||
import { ImageProcessor } from './image_processor.js';
|
||||
@@ -38,11 +37,7 @@ export class OfficeProcessor extends FileProcessor {
|
||||
try {
|
||||
log.info('Starting Office document text extraction...');
|
||||
|
||||
// Validate language format
|
||||
const language = options.language || this.getDefaultOCRLanguage();
|
||||
if (!this.isValidLanguageFormat(language)) {
|
||||
throw new Error(`Invalid OCR language format: ${language}. Use format like 'eng' or 'ron+eng'`);
|
||||
}
|
||||
const language = options.language || "eng";
|
||||
|
||||
// Extract text from Office document
|
||||
const data = await this.parseOfficeDocument(buffer);
|
||||
@@ -93,41 +88,4 @@ export class OfficeProcessor extends FileProcessor {
|
||||
async cleanup(): Promise<void> {
|
||||
await this.imageProcessor.cleanup();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get default OCR language from options
|
||||
*/
|
||||
private getDefaultOCRLanguage(): string {
|
||||
try {
|
||||
const ocrLanguage = options.getOption('ocrLanguage');
|
||||
if (!ocrLanguage) {
|
||||
throw new Error('OCR language not configured in user settings');
|
||||
}
|
||||
return ocrLanguage;
|
||||
} catch (error) {
|
||||
log.error(`Failed to get default OCR language: ${error}`);
|
||||
throw new Error('OCR language must be configured in settings before processing');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate OCR language format
|
||||
* Supports single language (eng) or multi-language (ron+eng)
|
||||
*/
|
||||
private isValidLanguageFormat(language: string): boolean {
|
||||
if (!language || typeof language !== 'string') {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Split by '+' for multi-language format
|
||||
const languages = language.split('+');
|
||||
|
||||
// Check each language code (should be 2-7 characters, alphanumeric with underscores)
|
||||
const validLanguagePattern = /^[a-zA-Z]{2,3}(_[a-zA-Z]{2,3})?$/;
|
||||
|
||||
return languages.every(lang => {
|
||||
const trimmed = lang.trim();
|
||||
return trimmed.length > 0 && validLanguagePattern.test(trimmed);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import pdfParse from 'pdf-parse';
|
||||
|
||||
import log from '../../log.js';
|
||||
import options from '../../options.js';
|
||||
import { OCRProcessingOptions,OCRResult } from '../ocr_service.js';
|
||||
import { FileProcessor } from './file_processor.js';
|
||||
import { ImageProcessor } from './image_processor.js';
|
||||
@@ -31,12 +30,6 @@ export class PDFProcessor extends FileProcessor {
|
||||
try {
|
||||
log.info('Starting PDF text extraction...');
|
||||
|
||||
// Validate language format
|
||||
const language = options.language || this.getDefaultOCRLanguage();
|
||||
if (!this.isValidLanguageFormat(language)) {
|
||||
throw new Error(`Invalid OCR language format: ${language}. Use format like 'eng' or 'ron+eng'`);
|
||||
}
|
||||
|
||||
// First try to extract existing text from PDF
|
||||
if (options.enablePDFTextExtraction !== false) {
|
||||
const textResult = await this.extractTextFromPDF(buffer, options);
|
||||
@@ -64,7 +57,7 @@ export class PDFProcessor extends FileProcessor {
|
||||
text: data.text.trim(),
|
||||
confidence: 0.99, // High confidence for direct text extraction
|
||||
extractedAt: new Date().toISOString(),
|
||||
language: options.language || this.getDefaultOCRLanguage(),
|
||||
language: options.language || "eng",
|
||||
pageCount: data.numpages
|
||||
};
|
||||
} catch (error) {
|
||||
@@ -91,7 +84,7 @@ export class PDFProcessor extends FileProcessor {
|
||||
text: '[PDF OCR not fully implemented - would convert PDF pages to images and OCR each page]',
|
||||
confidence: 0.0,
|
||||
extractedAt: new Date().toISOString(),
|
||||
language: options.language || this.getDefaultOCRLanguage(),
|
||||
language: options.language || "eng",
|
||||
pageCount: 1
|
||||
};
|
||||
} catch (error) {
|
||||
@@ -107,41 +100,4 @@ export class PDFProcessor extends FileProcessor {
|
||||
async cleanup(): Promise<void> {
|
||||
await this.imageProcessor.cleanup();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get default OCR language from options
|
||||
*/
|
||||
private getDefaultOCRLanguage(): string {
|
||||
try {
|
||||
const ocrLanguage = options.getOption('ocrLanguage');
|
||||
if (!ocrLanguage) {
|
||||
throw new Error('OCR language not configured in user settings');
|
||||
}
|
||||
return ocrLanguage;
|
||||
} catch (error) {
|
||||
log.error(`Failed to get default OCR language: ${error}`);
|
||||
throw new Error('OCR language must be configured in settings before processing');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate OCR language format
|
||||
* Supports single language (eng) or multi-language (ron+eng)
|
||||
*/
|
||||
private isValidLanguageFormat(language: string): boolean {
|
||||
if (!language || typeof language !== 'string') {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Split by '+' for multi-language format
|
||||
const languages = language.split('+');
|
||||
|
||||
// Check each language code (should be 2-7 characters, alphanumeric with underscores)
|
||||
const validLanguagePattern = /^[a-zA-Z]{2,3}(_[a-zA-Z]{2,3})?$/;
|
||||
|
||||
return languages.every(lang => {
|
||||
const trimmed = lang.trim();
|
||||
return trimmed.length > 0 && validLanguagePattern.test(trimmed);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import sharp from 'sharp';
|
||||
|
||||
import log from '../../log.js';
|
||||
import options from '../../options.js';
|
||||
import { OCRProcessingOptions,OCRResult } from '../ocr_service.js';
|
||||
import { FileProcessor } from './file_processor.js';
|
||||
import { ImageProcessor } from './image_processor.js';
|
||||
@@ -30,11 +29,7 @@ export class TIFFProcessor extends FileProcessor {
|
||||
try {
|
||||
log.info('Starting TIFF text extraction...');
|
||||
|
||||
// Validate language format
|
||||
const language = options.language || this.getDefaultOCRLanguage();
|
||||
if (!this.isValidLanguageFormat(language)) {
|
||||
throw new Error(`Invalid OCR language format: ${language}. Use format like 'eng' or 'ron+eng'`);
|
||||
}
|
||||
const language = options.language || "eng";
|
||||
|
||||
// Check if this is a multi-page TIFF
|
||||
const metadata = await sharp(buffer).metadata();
|
||||
@@ -75,7 +70,7 @@ export class TIFFProcessor extends FileProcessor {
|
||||
text: combinedText.trim(),
|
||||
confidence: averageConfidence,
|
||||
extractedAt: new Date().toISOString(),
|
||||
language: options.language || this.getDefaultOCRLanguage(),
|
||||
language,
|
||||
pageCount
|
||||
};
|
||||
|
||||
@@ -95,41 +90,4 @@ export class TIFFProcessor extends FileProcessor {
|
||||
async cleanup(): Promise<void> {
|
||||
await this.imageProcessor.cleanup();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get default OCR language from options
|
||||
*/
|
||||
private getDefaultOCRLanguage(): string {
|
||||
try {
|
||||
const ocrLanguage = options.getOption('ocrLanguage');
|
||||
if (!ocrLanguage) {
|
||||
throw new Error('OCR language not configured in user settings');
|
||||
}
|
||||
return ocrLanguage;
|
||||
} catch (error) {
|
||||
log.error(`Failed to get default OCR language: ${error}`);
|
||||
throw new Error('OCR language must be configured in settings before processing');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate OCR language format
|
||||
* Supports single language (eng) or multi-language (ron+eng)
|
||||
*/
|
||||
private isValidLanguageFormat(language: string): boolean {
|
||||
if (!language || typeof language !== 'string') {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Split by '+' for multi-language format
|
||||
const languages = language.split('+');
|
||||
|
||||
// Check each language code (should be 2-7 characters, alphanumeric with underscores)
|
||||
const validLanguagePattern = /^[a-zA-Z]{2,3}(_[a-zA-Z]{2,3})?$/;
|
||||
|
||||
return languages.every(lang => {
|
||||
const trimmed = lang.trim();
|
||||
return trimmed.length > 0 && validLanguagePattern.test(trimmed);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -216,7 +216,6 @@ const defaultOptions: DefaultOption[] = [
|
||||
|
||||
// OCR options
|
||||
{ name: "ocrEnabled", value: "false", isSynced: true },
|
||||
{ name: "ocrLanguage", value: "eng", isSynced: true },
|
||||
{ name: "ocrAutoProcessImages", value: "true", isSynced: true },
|
||||
{ name: "ocrMinConfidence", value: "0.55", isSynced: true },
|
||||
];
|
||||
|
||||
@@ -9,27 +9,29 @@ export interface Locale {
|
||||
devOnly?: boolean;
|
||||
/** The value to pass to `--lang` for the Electron instance in order to set it as a locale. Not setting it will hide it from the list of supported locales. */
|
||||
electronLocale?: "en" | "de" | "es" | "fr" | "zh_CN" | "zh_TW" | "ro" | "af" | "am" | "ar" | "bg" | "bn" | "ca" | "cs" | "da" | "el" | "en_GB" | "es_419" | "et" | "fa" | "fi" | "fil" | "gu" | "he" | "hi" | "hr" | "hu" | "id" | "it" | "ja" | "kn" | "ko" | "lt" | "lv" | "ml" | "mr" | "ms" | "nb" | "nl" | "pl" | "pt_BR" | "pt_PT" | "ru" | "sk" | "sl" | "sr" | "sv" | "sw" | "ta" | "te" | "th" | "tr" | "uk" | "ur" | "vi";
|
||||
/** The Tesseract OCR language code for this locale (e.g. "eng", "fra", "deu"). See https://tesseract-ocr.github.io/tessdoc/Data-Files-in-different-versions.html */
|
||||
tesseractCode?: "eng" | "deu" | "spa" | "fra" | "gle" | "ita" | "hin" | "jpn" | "por" | "pol" | "ron" | "rus" | "chi_sim" | "chi_tra" | "ukr" | "ara" | "heb" | "kur" | "fas" | "kor";
|
||||
}
|
||||
|
||||
// When adding a new locale, prefer the version with hyphen instead of underscore.
|
||||
const UNSORTED_LOCALES = [
|
||||
{ id: "cn", name: "简体中文", electronLocale: "zh_CN" },
|
||||
{ id: "de", name: "Deutsch", electronLocale: "de" },
|
||||
{ id: "en", name: "English (United States)", electronLocale: "en" },
|
||||
{ id: "en-GB", name: "English (United Kingdom)", electronLocale: "en_GB" },
|
||||
{ id: "es", name: "Español", electronLocale: "es" },
|
||||
{ id: "fr", name: "Français", electronLocale: "fr" },
|
||||
{ id: "ga", name: "Gaeilge", electronLocale: "en" },
|
||||
{ id: "it", name: "Italiano", electronLocale: "it" },
|
||||
{ id: "hi", name: "हिन्दी", electronLocale: "hi" },
|
||||
{ id: "ja", name: "日本語", electronLocale: "ja" },
|
||||
{ id: "pt_br", name: "Português (Brasil)", electronLocale: "pt_BR" },
|
||||
{ id: "pt", name: "Português (Portugal)", electronLocale: "pt_PT" },
|
||||
{ id: "pl", name: "Polski", electronLocale: "pl" },
|
||||
{ id: "ro", name: "Română", electronLocale: "ro" },
|
||||
{ id: "ru", name: "Русский", electronLocale: "ru" },
|
||||
{ id: "tw", name: "繁體中文", electronLocale: "zh_TW" },
|
||||
{ id: "uk", name: "Українська", electronLocale: "uk" },
|
||||
{ id: "cn", name: "简体中文", electronLocale: "zh_CN", tesseractCode: "chi_sim" },
|
||||
{ id: "de", name: "Deutsch", electronLocale: "de", tesseractCode: "deu" },
|
||||
{ id: "en", name: "English (United States)", electronLocale: "en", tesseractCode: "eng" },
|
||||
{ id: "en-GB", name: "English (United Kingdom)", electronLocale: "en_GB", tesseractCode: "eng" },
|
||||
{ id: "es", name: "Español", electronLocale: "es", tesseractCode: "spa" },
|
||||
{ id: "fr", name: "Français", electronLocale: "fr", tesseractCode: "fra" },
|
||||
{ id: "ga", name: "Gaeilge", electronLocale: "en", tesseractCode: "gle" },
|
||||
{ id: "it", name: "Italiano", electronLocale: "it", tesseractCode: "ita" },
|
||||
{ id: "hi", name: "हिन्दी", electronLocale: "hi", tesseractCode: "hin" },
|
||||
{ id: "ja", name: "日本語", electronLocale: "ja", tesseractCode: "jpn" },
|
||||
{ id: "pt_br", name: "Português (Brasil)", electronLocale: "pt_BR", tesseractCode: "por" },
|
||||
{ id: "pt", name: "Português (Portugal)", electronLocale: "pt_PT", tesseractCode: "por" },
|
||||
{ id: "pl", name: "Polski", electronLocale: "pl", tesseractCode: "pol" },
|
||||
{ id: "ro", name: "Română", electronLocale: "ro", tesseractCode: "ron" },
|
||||
{ id: "ru", name: "Русский", electronLocale: "ru", tesseractCode: "rus" },
|
||||
{ id: "tw", name: "繁體中文", electronLocale: "zh_TW", tesseractCode: "chi_tra" },
|
||||
{ id: "uk", name: "Українська", electronLocale: "uk", tesseractCode: "ukr" },
|
||||
|
||||
/**
|
||||
* Development-only languages.
|
||||
@@ -53,25 +55,29 @@ const UNSORTED_LOCALES = [
|
||||
id: "ar",
|
||||
name: "اَلْعَرَبِيَّةُ",
|
||||
rtl: true,
|
||||
electronLocale: "ar"
|
||||
electronLocale: "ar",
|
||||
tesseractCode: "ara"
|
||||
},
|
||||
{ // Hebrew
|
||||
id: "he",
|
||||
name: "עברית",
|
||||
rtl: true,
|
||||
contentOnly: true
|
||||
contentOnly: true,
|
||||
tesseractCode: "heb"
|
||||
},
|
||||
{ // Kurdish
|
||||
id: "ku",
|
||||
name: "کوردی",
|
||||
rtl: true,
|
||||
contentOnly: true
|
||||
contentOnly: true,
|
||||
tesseractCode: "kur"
|
||||
},
|
||||
{ // Persian
|
||||
id: "fa",
|
||||
name: "فارسی",
|
||||
rtl: true,
|
||||
contentOnly: true
|
||||
contentOnly: true,
|
||||
tesseractCode: "fas"
|
||||
}
|
||||
] as const;
|
||||
|
||||
@@ -82,3 +88,10 @@ export const LOCALES: Locale[] = Array.from(UNSORTED_LOCALES)
|
||||
export type LOCALE_IDS = typeof UNSORTED_LOCALES[number]["id"];
|
||||
/** A type containing a string union of all the supported locales that are not content-only (i.e. can be used as the UI language). */
|
||||
export type DISPLAYABLE_LOCALE_IDS = Exclude<typeof UNSORTED_LOCALES[number], { contentOnly: true }>["id"];
|
||||
|
||||
/**
|
||||
* Returns the Tesseract OCR language code for the given locale ID, or `null` if not mapped.
|
||||
*/
|
||||
export function getTesseractCode(localeId: string): string | null {
|
||||
return LOCALES.find((l) => l.id === localeId)?.tesseractCode ?? null;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user