@mtorange/mcp-local-file-search
Version:
Local file indexing and search MCP server with BM25 algorithm and multilingual support
219 lines (182 loc) • 7.67 kB
JavaScript
const fs = require('fs-extra');
const path = require('path');
class I18n {
constructor() {
this.currentLocale = this.detectLocale();
this.messages = this.loadMessages();
}
// 언어 환경 자동 감지
detectLocale() {
// 1. 사용자 정의 환경 변수 (최우선)
if (process.env.MCP_LANG) {
return this.normalizeLocale(process.env.MCP_LANG);
}
// 2. 시스템 환경 변수들
const envVars = [
'LANGUAGE', // 언어 우선순위
'LC_ALL', // 모든 로케일
'LC_MESSAGES', // 메시지 언어
'LANG' // 시스템 언어
];
for (const envVar of envVars) {
if (process.env[envVar]) {
return this.normalizeLocale(process.env[envVar]);
}
}
// 3. Node.js 내장 API
try {
const locale = Intl.DateTimeFormat().resolvedOptions().locale;
if (locale) {
return this.normalizeLocale(locale);
}
} catch (error) {
// Intl API 사용 불가 시 무시
}
// 4. 기본값 (영어)
return 'en';
}
// 로케일 정규화 (ko_KR.UTF-8 -> ko, en_US -> en)
normalizeLocale(locale) {
if (!locale) return 'en';
const normalized = locale.toLowerCase().split(/[_\-.]/)[0];
// 지원하는 언어 목록
const supportedLanguages = ['ko', 'en', 'ja', 'zh'];
if (supportedLanguages.includes(normalized)) {
return normalized;
}
// 지원하지 않는 언어는 영어로 기본값
return 'en';
}
// 메시지 로드
loadMessages() {
return {
ko: {
// 일반 메시지
'indexing.start': '인덱스가 없어 자동 인덱싱을 시작합니다...',
'indexing.complete': '인덱싱이 완료되었습니다.',
'indexing.reindex': '파일을 다시 인덱싱합니다...',
'indexing.force': '강제 재인덱싱을 시작합니다...',
// 검색 관련
'search.start': '검색을 시작합니다...',
'search.complete': '검색이 완료되었습니다.',
'search.no_results': '검색 결과가 없습니다.',
'search.results': '검색 결과 ({count}개):',
'search.in_file': '파일에서 검색 중: {file}',
// 오류 메시지
'error.index_not_found': '인덱스 파일을 찾을 수 없습니다.',
'error.file_not_found': '파일을 찾을 수 없습니다: {file}',
'error.parse_failed': '파일 파싱에 실패했습니다: {file}',
'error.mcp_server': 'MCP 서버 시작 중 오류가 발생했습니다.',
'error.uncaught_exception': '처리되지 않은 예외가 발생했습니다.',
'error.unhandled_rejection': '처리되지 않은 Promise 거부가 발생했습니다.',
// MCP 도구 설명
'tool.search_local.description': '로컬 파일에서 텍스트를 검색합니다.',
'tool.search_local.query': '검색할 텍스트',
'tool.search_local.limit': '검색 결과 최대 개수 (기본값: 10)',
'tool.search_in_file.description': '특정 파일에서 텍스트를 검색합니다.',
'tool.search_in_file.file_path': '검색할 파일 경로',
'tool.search_in_file.query': '검색할 텍스트',
'tool.get_index_stats.description': '인덱스 통계를 조회합니다.',
'tool.find_similar_files.description': '특정 파일과 유사한 파일들을 찾습니다.',
'tool.find_similar_files.file_path': '기준 파일 경로',
'tool.find_similar_files.limit': '결과 최대 개수 (기본값: 5)',
'tool.reindex.description': '파일을 다시 인덱싱합니다.',
'tool.reindex.force': '강제 재인덱싱 여부 (기본값: false)',
// 상태 메시지
'status.initializing': '초기화 중...',
'status.ready': '준비 완료',
'status.processing': '처리 중...',
'status.indexing': '인덱싱 중...',
'status.searching': '검색 중...'
},
en: {
// General messages
'indexing.start': 'Starting automatic indexing as no index exists...',
'indexing.complete': 'Indexing completed.',
'indexing.reindex': 'Reindexing files...',
'indexing.force': 'Starting forced reindexing...',
// Search related
'search.start': 'Starting search...',
'search.complete': 'Search completed.',
'search.no_results': 'No search results found.',
'search.results': 'Search results ({count} items):',
'search.in_file': 'Searching in file: {file}',
// Error messages
'error.index_not_found': 'Index file not found.',
'error.file_not_found': 'File not found: {file}',
'error.parse_failed': 'Failed to parse file: {file}',
'error.mcp_server': 'Error occurred while starting MCP server.',
'error.uncaught_exception': 'Uncaught exception occurred.',
'error.unhandled_rejection': 'Unhandled promise rejection occurred.',
// MCP tool descriptions
'tool.search_local.description': 'Search for text in local files.',
'tool.search_local.query': 'Text to search for',
'tool.search_local.limit': 'Maximum number of search results (default: 10)',
'tool.search_in_file.description': 'Search for text in a specific file.',
'tool.search_in_file.file_path': 'File path to search in',
'tool.search_in_file.query': 'Text to search for',
'tool.get_index_stats.description': 'Get index statistics.',
'tool.find_similar_files.description': 'Find files similar to a specific file.',
'tool.find_similar_files.file_path': 'Reference file path',
'tool.find_similar_files.limit': 'Maximum number of results (default: 5)',
'tool.reindex.description': 'Reindex files.',
'tool.reindex.force': 'Force reindexing (default: false)',
// Status messages
'status.initializing': 'Initializing...',
'status.ready': 'Ready',
'status.processing': 'Processing...',
'status.indexing': 'Indexing...',
'status.searching': 'Searching...'
}
};
}
// 메시지 번역
t(key, params = {}) {
const messages = this.messages[this.currentLocale] || this.messages.en;
let message = messages[key];
if (!message) {
// 키가 없으면 영어 메시지 시도
message = this.messages.en[key];
}
if (!message) {
// 영어 메시지도 없으면 키 자체 반환
return key;
}
// 파라미터 치환
for (const [param, value] of Object.entries(params)) {
message = message.replace(new RegExp(`{${param}}`, 'g'), value);
}
return message;
}
// 현재 로케일 가져오기
getLocale() {
return this.currentLocale;
}
// 로케일 설정
setLocale(locale) {
this.currentLocale = this.normalizeLocale(locale);
}
// 언어 감지 정보 반환 (디버깅용)
getLocaleInfo() {
return {
detected: this.currentLocale,
env: {
MCP_LANG: process.env.MCP_LANG,
LANGUAGE: process.env.LANGUAGE,
LC_ALL: process.env.LC_ALL,
LC_MESSAGES: process.env.LC_MESSAGES,
LANG: process.env.LANG
},
intl: (() => {
try {
return Intl.DateTimeFormat().resolvedOptions().locale;
} catch {
return 'unavailable';
}
})()
};
}
}
// 싱글톤 인스턴스 생성
const i18n = new I18n();
module.exports = i18n;