🚀 Socket Launch Week Day 5:Introducing Repository Access Permissions and Custom Roles.Learn more
Sign In

xtrans

Package Overview
Dependencies
Maintainers
1
Versions
5
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

xtrans - npm Package Compare versions

Comparing version
1.1.0
to
1.2.0
+19
dist/prompts/base-dictionary.d.ts
import { ParseOptions, PromptStrategy } from '../interfaces/prompt-strategy.js';
import { StandardBoxData, TranslationResult } from '../providers/types.js';
/**
* Base class for all dictionary-style prompt strategies.
* Provides shared parsing and rendering logic for dictionary entries.
* Subclasses only need to override `id` and `getSystemMessage()`.
*/
export declare abstract class BaseDictionaryStrategy implements PromptStrategy {
abstract id: string;
protected createRawResult(): TranslationResult;
createStreamParser(options?: ParseOptions): {
feed: (chunk: string) => void;
getResult: () => StandardBoxData;
};
getInitialState(): StandardBoxData;
abstract getSystemMessage(targetLang: string): string;
parse(text: string, options?: ParseOptions): StandardBoxData;
protected toStandardBoxData(tr: TranslationResult, targetLang: string): StandardBoxData;
}
import { TagStreamParser } from '../utils/tag-parser.js';
/**
* Base class for all dictionary-style prompt strategies.
* Provides shared parsing and rendering logic for dictionary entries.
* Subclasses only need to override `id` and `getSystemMessage()`.
*/
export class BaseDictionaryStrategy {
createRawResult() {
return {
antonyms: [],
definitions: [],
examples: [],
idioms: [],
items: [],
meaning: [],
phonetic: '',
synonyms: [],
tags: [],
word: ''
};
}
createStreamParser(options) {
const targetLang = options?.targetLang || 'zh';
const raw = this.createRawResult();
let currentDefinition = null;
const addItem = (type, value) => {
raw.items = raw.items || [];
raw.items.push({ type, value });
};
const parser = new TagStreamParser((event) => {
switch (event.type) {
case 'block_end': {
currentDefinition = null;
break;
}
case 'block_start': {
if (event.name === 'DEF') {
currentDefinition = { examples: [], meaning: '', partOfSpeech: '' };
raw.definitions.push(currentDefinition);
addItem('definition', currentDefinition);
}
break;
}
case 'tag': {
switch (event.key) {
case 'ANTONYM': {
raw.antonyms.push(event.value);
addItem('antonym', event.value);
break;
}
case 'EXAMPLE': {
if (currentDefinition) {
currentDefinition.examples.push(event.value);
}
raw.examples.push(event.value);
break;
}
case 'IDIOM': {
raw.idioms.push(event.value);
addItem('idiom', event.value);
break;
}
case 'MEANING': {
if (currentDefinition) {
currentDefinition.meaning = event.value;
}
raw.meaning.push(event.value);
break;
}
case 'ORIGIN': {
raw.origin = event.value;
addItem('origin', event.value);
break;
}
case 'PHONETIC': {
raw.phonetic = event.value;
break;
}
case 'POS': {
if (currentDefinition) {
currentDefinition.partOfSpeech = event.value;
}
else {
currentDefinition = { examples: [], meaning: '', partOfSpeech: event.value };
raw.definitions.push(currentDefinition);
addItem('definition', currentDefinition);
}
break;
}
case 'SYNONYM': {
raw.synonyms.push(event.value);
addItem('synonym', event.value);
break;
}
case 'TAG': {
raw.tags.push(event.value);
break;
}
case 'WORD': {
raw.word = event.value;
break;
}
}
break;
}
// No default
}
});
return {
feed: (chunk) => parser.feed(chunk),
getResult: () => this.toStandardBoxData(raw, targetLang)
};
}
getInitialState() {
return {
content: [],
tags: [],
title: ''
};
}
parse(text, options) {
const parserWrapper = this.createStreamParser(options);
parserWrapper.feed(text);
return parserWrapper.getResult();
}
toStandardBoxData(tr, targetLang) {
const sections = [];
const isChinese = targetLang.toLowerCase().startsWith('zh');
const synonymsLabel = isChinese ? '同义词' : 'Synonyms';
const antonymsLabel = isChinese ? '反义词' : 'Antonyms';
const originLabel = isChinese ? '词源' : 'Origin';
const idiomsLabel = isChinese ? '成语' : 'Idioms';
const examplesLabel = isChinese ? '示例' : 'Examples';
// Use ordered items if available (preserves LLM output order)
if (tr.items && tr.items.length > 0) {
let lastSection = null;
let lastType = null;
for (const item of tr.items) {
switch (item.type) {
case 'antonym': {
if (lastType === 'antonym' && lastSection) {
const prevContent = lastSection.content[0];
prevContent.text += `, ${item.value}`;
}
else {
const section = { content: [{ style: 'dim', text: `${antonymsLabel}: ${item.value}` }] };
sections.push(section);
lastSection = section;
lastType = 'antonym';
}
break;
}
case 'definition': {
const def = item.value;
const content = [{ style: 'green', text: ` • ${def.meaning}` }];
if (def.examples && def.examples.length > 0) {
for (const ex of def.examples) {
content.push({ style: 'dim', text: ` - ${ex}` });
}
}
const section = {
content,
title: { style: 'cyan-bold', text: def.partOfSpeech },
};
sections.push(section);
lastSection = section;
lastType = 'definition';
break;
}
case 'idiom': {
if (lastType === 'idiom' && lastSection) {
lastSection.content.push({ style: 'dim', text: `• ${item.value}` });
}
else {
const section = {
content: [{ style: 'dim', text: `• ${item.value}` }],
title: { style: 'magenta-bold', text: `${idiomsLabel}:` }
};
sections.push(section);
lastSection = section;
lastType = 'idiom';
}
break;
}
case 'origin': {
if (lastType === 'origin' && lastSection) {
const prevContent = lastSection.content[0];
prevContent.text += ` ${item.value}`;
}
else {
const section = { content: [{ style: 'dim', text: `${originLabel}: ${item.value}` }] };
sections.push(section);
lastSection = section;
lastType = 'origin';
}
break;
}
case 'synonym': {
if (lastType === 'synonym' && lastSection) {
const prevContent = lastSection.content[0];
prevContent.text += `, ${item.value}`;
}
else {
const section = { content: [{ style: 'dim', text: `${synonymsLabel}: ${item.value}` }] };
sections.push(section);
lastSection = section;
lastType = 'synonym';
}
break;
}
// No default
}
}
return {
content: sections,
subtitle: tr.phonetic,
tags: tr.tags,
title: tr.word,
};
}
// Fallback: Legacy rendering (grouped by type)
if (tr.definitions && tr.definitions.length > 0) {
for (const def of tr.definitions) {
const content = [{ style: 'green', text: ` • ${def.meaning}` }];
if (def.examples && def.examples.length > 0) {
for (const ex of def.examples) {
content.push({ style: 'dim', text: ` - ${ex}` });
}
}
sections.push({
content,
title: { style: 'cyan-bold', text: def.partOfSpeech },
});
}
}
else if (tr.meaning && tr.meaning.length > 0) {
const content = tr.meaning.map(m => ({ style: 'green', text: `• ${m}` }));
if (tr.examples && tr.examples.length > 0) {
content.push('', { style: 'yellow', text: `${examplesLabel}:` });
for (const ex of tr.examples) {
content.push({ style: 'dim', text: `• ${ex}` });
}
}
sections.push({ content });
}
if (tr.idioms && tr.idioms.length > 0) {
const content = tr.idioms.map(i => ({ style: 'dim', text: `• ${i}` }));
sections.push({
content,
title: { style: 'magenta-bold', text: `${idiomsLabel}:` },
});
}
if (!tr.items || tr.items.length === 0) {
if (tr.synonyms && tr.synonyms.length > 0) {
sections.push({ content: [{ style: 'dim', text: `${synonymsLabel}: ${tr.synonyms.join(', ')}` }] });
}
if (tr.antonyms && tr.antonyms.length > 0) {
sections.push({ content: [{ style: 'dim', text: `${antonymsLabel}: ${tr.antonyms.join(', ')}` }] });
}
if (tr.origin) {
sections.push({ content: [{ style: 'dim', text: `${originLabel}: ${tr.origin}` }] });
}
}
return {
content: sections,
subtitle: tr.phonetic,
tags: tr.tags,
title: tr.word,
};
}
}
import { BaseDictionaryStrategy } from './base-dictionary.js';
export declare class CambridgePromptStrategy extends BaseDictionaryStrategy {
id: string;
getSystemMessage(targetLang: string): string;
}
import { BaseDictionaryStrategy } from './base-dictionary.js';
export class CambridgePromptStrategy extends BaseDictionaryStrategy {
id = 'cambridge';
getSystemMessage(targetLang) {
return `
You are simulating the Cambridge Dictionary, the world's leading learner's dictionary.
Provide a Cambridge-style entry for the user input. Translate into "${targetLang}".
Style Guidelines:
- CLEAR, SIMPLE definitions that language learners can understand easily.
- Show BOTH UK and US pronunciation when they differ.
- Include CEFR level (A1-C2) to indicate difficulty.
- Grammar patterns: show how the word is used in structures (e.g. [T], [I], [+ to infinitive]).
- Common mistakes or usage notes for learners.
- Examples should use simple, natural English.
CRITICAL INSTRUCTION:
- Response MUST be a stream of data lines using tags.
- DO NOT use JSON.
- STRICTLY FOLLOW THE STRUCTURE BELOW.
[WORD] The original source word ONLY
[PHONETIC] IPA: UK /.../ US /.../ (show both if different)
[TAG] CEFR level (A1, A2, B1, B2, C1, C2) and grammar info. One per line.
[BLOCK:DEF]
[POS] Part of Speech with grammar pattern (e.g. "verb [T]", "noun [C]", "noun [U]")
[MEANING] Simple, clear definition in target language. Use easy words to explain difficult ones.
[EXAMPLE] Natural example sentence with translation
[END]
[SYNONYM] Synonym with usage note - One per line
[IDIOM] Common phrase or expression - One per line
Example Output:
[WORD] hello
[PHONETIC] UK /heˈləʊ/ US /heˈloʊ/
[TAG] A1
[TAG] Greeting
[BLOCK:DEF]
[POS] exclamation
[MEANING] 你好,用于见面时的问候或打电话时的开头语 (used when you meet someone or start a phone conversation)
[EXAMPLE] Hello, how are you? 你好,你怎么样?
[END]
[BLOCK:DEF]
[POS] noun [C]
[MEANING] 问候,打招呼 (something you say or do to greet someone)
[EXAMPLE] I said hello but she didn't answer. 我打了声招呼但她没回应。
[END]
[SYNONYM] hi - less formal
[SYNONYM] hey - very informal, used with friends
[IDIOM] say hello 打招呼
`;
}
}
import { BaseDictionaryStrategy } from './base-dictionary.js';
export declare class YoudaoPromptStrategy extends BaseDictionaryStrategy {
id: string;
getSystemMessage(targetLang: string): string;
}
import { BaseDictionaryStrategy } from './base-dictionary.js';
export class YoudaoPromptStrategy extends BaseDictionaryStrategy {
id = 'youdao';
getSystemMessage(targetLang) {
return `
You are simulating Youdao Dictionary (有道词典), the most popular Chinese-English dictionary.
Provide a Youdao-style entry for the user input. Translate into "${targetLang}".
Style Guidelines:
- Prioritize PRACTICAL, everyday usage over academic precision.
- Show CONCISE Chinese translations first, then brief English explanations.
- Include internet slang, colloquial usage, and modern expressions where relevant.
- Examples should be short, practical sentences from daily life.
- Keep definitions compact: focus on the most common 2-3 meanings.
CRITICAL INSTRUCTION:
- Response MUST be a stream of data lines using tags.
- DO NOT use JSON.
- STRICTLY FOLLOW THE STRUCTURE BELOW.
[WORD] The original source word ONLY
[PHONETIC] IPA phonetic transcription
[TAG] Usage frequency or domain tags (e.g. "CET4", "Daily", "Internet", "Business"). One per line.
[BLOCK:DEF]
[POS] Part of Speech
[MEANING] Concise translation in target language (keep it short, like "你好;喂")
[EXAMPLE] Short practical example with translation
[END]
[SYNONYM] Synonym - One per line
[IDIOM] Common phrase or collocation - One per line
Example Output:
[WORD] hello
[PHONETIC] /hɛˈloʊ/
[TAG] CET4
[TAG] Daily
[BLOCK:DEF]
[POS] int.
[MEANING] 你好;喂(用于问候或打电话)
[EXAMPLE] Hello! How are you? 你好!你怎么样?
[END]
[BLOCK:DEF]
[POS] n.
[MEANING] 招呼;问候
[EXAMPLE] She gave me a friendly hello. 她友好地跟我打了个招呼。
[END]
[SYNONYM] hi - 口语常用
[SYNONYM] hey - 非正式
[IDIOM] say hello to 向…问好
[IDIOM] hello world 你好世界(编程术语)
`;
}
}
export type TagEvent = {
content: string;
type: 'text';
} | {
key: string;
type: 'tag';
value: string;
} | {
name: string;
type: 'block_start';
} | {
type: 'block_end';
};
export declare class TagStreamParser {
private onEvent;
private buffer;
constructor(onEvent: (event: TagEvent) => void);
feed(chunk: string): void;
private parseLine;
private process;
}
export class TagStreamParser {
onEvent;
buffer = '';
constructor(onEvent) {
this.onEvent = onEvent;
}
feed(chunk) {
this.buffer += chunk;
this.process();
}
parseLine(line) {
if (!line)
return;
// Match [TAG] Value or [TAG]
const match = line.match(/^\[([A-Z0-9_:-]+)\]\s*(.*)$/);
if (match) {
const tag = match[1];
const value = match[2];
if (tag.startsWith('BLOCK:')) {
this.onEvent({ name: tag.replace('BLOCK:', ''), type: 'block_start' });
}
else if (tag === 'END') {
this.onEvent({ type: 'block_end' });
}
else {
this.onEvent({ key: tag, type: 'tag', value });
}
}
else {
// Treat as plain text content (maybe continuation of previous tag?)
// For strict protocol, we might ignore, but for robustness we emit text
this.onEvent({ content: line, type: 'text' });
}
}
process() {
let newlineIndex;
while ((newlineIndex = this.buffer.indexOf('\n')) !== -1) {
const line = this.buffer.slice(0, newlineIndex).trim();
this.buffer = this.buffer.slice(newlineIndex + 1);
this.parseLine(line);
}
}
}
+14
-10

@@ -45,4 +45,2 @@ import { confirm } from '@inquirer/prompts';

const mode = configService.getMode();
// Strategies (Hardcoded for now, but ready for config)
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const promptStrategy = PromptFactory.get(mode);

@@ -59,3 +57,4 @@ const renderStrategy = RendererFactory.get('box');

const stream = provider.stream(inputText, { systemMessage });
let buffer = '';
// Use streaming parser if available
const streamParser = promptStrategy.createStreamParser?.({ targetLang });
let result = promptStrategy.getInitialState();

@@ -65,4 +64,10 @@ let lastRender = 0;

for await (const chunk of stream) {
buffer += chunk;
result = promptStrategy.parse(buffer);
if (streamParser) {
streamParser.feed(chunk);
result = streamParser.getResult();
}
else {
// Fallback: accumulate and parse
result = promptStrategy.parse(chunk, { targetLang });
}
const now = Date.now();

@@ -74,6 +79,5 @@ if (now - lastRender > 50) {

}
// Final Render
// Force override word with input text (Specific to Translation Strategy)
if (result && typeof result === 'object' && 'word' in result) {
result.word = inputText;
// Final result - override title with input text for word lookups
if (result.title === '' || !result.title) {
result.title = inputText;
}

@@ -83,3 +87,3 @@ renderStrategy.render(result, false);

spinner.stop();
// 2. Save History
// Save History
historyService.save(inputText, result, config.current, targetLang);

@@ -86,0 +90,0 @@ }

@@ -1,6 +0,14 @@

export interface PromptStrategy<T = any> {
getInitialState(): T;
import { StandardBoxData } from '../providers/types.js';
export interface ParseOptions {
targetLang?: string;
}
export interface PromptStrategy {
createStreamParser?(options?: ParseOptions): {
feed(chunk: string): void;
getResult(): StandardBoxData;
};
getInitialState(): StandardBoxData;
getSystemMessage(targetLang: string): string;
id: string;
parse(text: string): T;
parse(text: string, options?: ParseOptions): StandardBoxData;
}

@@ -1,5 +0,6 @@

export interface RenderStrategy<T = any> {
import { StandardBoxData } from '../providers/types.js';
export interface RenderStrategy {
id: string;
render(data: T, fromCache: boolean): void;
render(data: StandardBoxData, fromCache: boolean): void;
reset(): void;
}

@@ -1,8 +0,13 @@

import { PromptStrategy } from '../interfaces/prompt-strategy.js';
import { ParseOptions, PromptStrategy } from '../interfaces/prompt-strategy.js';
import { StandardBoxData } from '../providers/types.js';
export declare class GrammarPromptStrategy implements PromptStrategy<StandardBoxData> {
export declare class GrammarPromptStrategy implements PromptStrategy {
id: string;
createStreamParser(_options?: ParseOptions): {
feed: (chunk: string) => void;
getResult: () => StandardBoxData;
};
getInitialState(): StandardBoxData;
getSystemMessage(_targetLang: string): string;
parse(text: string): StandardBoxData;
parse(text: string, options?: ParseOptions): StandardBoxData;
private toStandardBoxData;
}

@@ -0,3 +1,33 @@

import { TagStreamParser } from '../utils/tag-parser.js';
export class GrammarPromptStrategy {
id = 'grammar';
createStreamParser(_options) {
const raw = {
corrected: '',
improvements: [],
mistakes: []
};
const parser = new TagStreamParser((event) => {
if (event.type === 'tag') {
switch (event.key) {
case 'CORRECT': {
raw.corrected = event.value;
break;
}
case 'IMPROVE': {
raw.improvements.push(event.value);
break;
}
case 'MISTAKE': {
raw.mistakes.push(event.value);
break;
}
}
}
});
return {
feed: (chunk) => parser.feed(chunk),
getResult: () => this.toStandardBoxData(raw)
};
}
getInitialState() {

@@ -32,42 +62,24 @@ return {

}
parse(text) {
const lines = text.split('\n');
// We recreate state each parse for simplicity,
// real streaming might want to accumulate content.
// For now, this is stateless parsing of the full accumulated buffer.
let corrected = '';
const mistakes = [];
const improvements = [];
for (const line of lines) {
const clean = line.trim();
if (!clean)
continue;
if (clean.startsWith('[CORRECT]')) {
corrected = clean.replace('[CORRECT]', '').trim();
}
else if (clean.startsWith('[MISTAKE]')) {
mistakes.push(clean.replace('[MISTAKE]', '').trim());
}
else if (clean.startsWith('[IMPROVE]')) {
improvements.push(clean.replace('[IMPROVE]', '').trim());
}
}
parse(text, options) {
const parserWrapper = this.createStreamParser(options);
parserWrapper.feed(text);
return parserWrapper.getResult();
}
toStandardBoxData(raw) {
const sections = [];
if (corrected) {
if (raw.corrected) {
sections.push({
content: corrected,
style: 'code',
content: [{ style: 'code', text: raw.corrected }],
title: 'Correction'
});
}
if (mistakes.length > 0) {
if (raw.mistakes.length > 0) {
sections.push({
content: mistakes.map(m => `• ${m}`),
content: raw.mistakes.map(m => `• ${m}`),
title: 'Analysis'
});
}
if (improvements.length > 0) {
if (raw.improvements.length > 0) {
sections.push({
content: improvements.map(i => `• ${i}`),
style: 'dim',
content: raw.improvements.map(i => ({ style: 'dim', text: `• ${i}` })),
title: 'Suggestions'

@@ -74,0 +86,0 @@ });

import { PromptStrategy } from '../interfaces/prompt-strategy.js';
export declare const PromptFactory: {
get(id: string): PromptStrategy<any>;
register(strategy: PromptStrategy<any>): void;
get(id: string): PromptStrategy;
register(strategy: PromptStrategy): void;
};

@@ -0,12 +1,14 @@

import { CambridgePromptStrategy } from './cambridge.js';
import { GrammarPromptStrategy } from './grammar.js';
import { OxfordPromptStrategy } from './oxford.js';
import { TranslationPromptStrategy } from './translation.js';
// eslint-disable-next-line @typescript-eslint/no-explicit-any
import { YoudaoPromptStrategy } from './youdao.js';
const strategies = {
'cambridge': new CambridgePromptStrategy(),
'grammar': new GrammarPromptStrategy(),
'oxford': new OxfordPromptStrategy(),
'translation': new TranslationPromptStrategy(),
'youdao': new YoudaoPromptStrategy(),
};
export const PromptFactory = {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
get(id) {

@@ -19,3 +21,2 @@ const strategy = strategies[id];

},
// eslint-disable-next-line @typescript-eslint/no-explicit-any
register(strategy) {

@@ -22,0 +23,0 @@ strategies[strategy.id] = strategy;

@@ -1,8 +0,5 @@

import { PromptStrategy } from '../interfaces/prompt-strategy.js';
import { StandardBoxData } from '../providers/types.js';
export declare class OxfordPromptStrategy implements PromptStrategy<StandardBoxData> {
import { BaseDictionaryStrategy } from './base-dictionary.js';
export declare class OxfordPromptStrategy extends BaseDictionaryStrategy {
id: string;
getInitialState(): StandardBoxData;
getSystemMessage(_targetLang: string): string;
parse(text: string): StandardBoxData;
getSystemMessage(targetLang: string): string;
}

@@ -1,88 +0,61 @@

export class OxfordPromptStrategy {
import { BaseDictionaryStrategy } from './base-dictionary.js';
export class OxfordPromptStrategy extends BaseDictionaryStrategy {
id = 'oxford';
getInitialState() {
return {
content: [],
tags: ['Oxford'],
title: 'Oxford Dictionary',
};
}
getSystemMessage(_targetLang) {
getSystemMessage(targetLang) {
return `
You are the Oxford English Dictionary. Provide a comprehensive, academic definition of the word.
Focus on: British English pronunciation (IPA), precise definitions, etymology, and usage labels (e.g., formal, archaic).
You are simulating the Oxford English Dictionary (OED), the definitive historical dictionary of English.
Provide an Oxford-style scholarly entry for the user input. Translate into "${targetLang}".
Output Format (Stream of tags):
[WORD] The word
[PHONETIC] IPA (British)
[POS] Part of Speech (noun, verb, etc.)
[DEF] Definition (numbered)
[EXAMPLE] Usage example (italics preferred in rendering, but plain text here)
[ETYMOLOGY] Origin/History of the word
[LEVEL] CEFR Level (A1-C2) or Frequency label if applicable
Style Guidelines:
- FORMAL, AUTHORITATIVE, and PRECISE academic definitions.
- Prefer British English pronunciation and spelling.
- Include DETAILED etymology tracing the word's origin through history.
- Usage labels: formal, informal, archaic, literary, technical, dated, etc.
- Definitions ordered HISTORICALLY (earliest meaning first, then derived meanings).
- Examples should include literary or historical citations where possible.
Structure:
- Start with [WORD] and [PHONETIC].
- Then [POS].
- Under [POS], list [DEF] and [EXAMPLE] pairs.
- End with [ETYMOLOGY] and [LEVEL].
CRITICAL INSTRUCTION:
- Response MUST be a stream of data lines using tags.
- DO NOT use JSON.
- STRICTLY FOLLOW THE STRUCTURE BELOW.
[WORD] The original source word ONLY
[PHONETIC] IPA phonetic (British RP preferred, e.g. /həˈləʊ/)
[TAG] Register and usage labels (e.g. "Formal", "British", "Literary", "Archaic"). One per line.
[BLOCK:DEF]
[POS] Part of Speech (with formal labels: n., v., adj., adv., int., etc.)
[MEANING] Scholarly definition in target language with English clarification in parentheses
[EXAMPLE] Literary or historical citation with source attribution if possible
[END]
[SYNONYM] Synonym with register distinction - One per line
[ANTONYM] Antonym with register distinction - One per line
[ORIGIN] DETAILED etymology: Proto-language roots → Old/Middle English evolution → modern usage. Include dates of first attestation.
Example Output:
[WORD] hello
[PHONETIC] /həˈləʊ/
[TAG] Informal
[TAG] British
[BLOCK:DEF]
[POS] int.
[MEANING] 用以打招呼或引起注意 (Used as a greeting or to attract attention)
[EXAMPLE] "Hello, old fellow!" — P.G. Wodehouse, 1915
[END]
[BLOCK:DEF]
[POS] n.
[MEANING] 招呼,问候之语 (An utterance of 'hello'; a greeting)
[EXAMPLE] She called out a cheerful hello. 她愉快地打了声招呼。
[END]
[SYNONYM] salutation - Formal
[SYNONYM] greeting - Neutral
[ANTONYM] farewell - Formal
[ANTONYM] goodbye - Neutral
[ORIGIN] Mid 19th century (1827): variant of earlier hollo, hallo; related to Old High German halā, holā (emphatic imperative of holen 'to fetch'). First recorded use in American English. Popularized as telephone greeting by Thomas Edison (1877).
`;
}
parse(text) {
const lines = text.split('\n');
const result = this.getInitialState();
let currentPosSection = null;
let etymology = '';
let level = '';
// Temporary storage to build sections
const posSections = [];
for (const line of lines) {
const clean = line.trim();
if (!clean)
continue;
if (clean.startsWith('[WORD]')) {
result.title = clean.replace('[WORD]', '').trim();
}
else if (clean.startsWith('[PHONETIC]')) {
result.subtitle = clean.replace('[PHONETIC]', '').trim();
}
else if (clean.startsWith('[LEVEL]')) {
level = clean.replace('[LEVEL]', '').trim();
if (level)
result.tags?.push(level);
}
else if (clean.startsWith('[POS]')) {
const pos = clean.replace('[POS]', '').trim();
currentPosSection = { content: [], title: pos };
posSections.push(currentPosSection);
}
else if (clean.startsWith('[DEF]')) {
const def = clean.replace('[DEF]', '').trim();
if (currentPosSection) {
currentPosSection.content.push(`• ${def}`);
}
}
else if (clean.startsWith('[EXAMPLE]')) {
const ex = clean.replace('[EXAMPLE]', '').trim();
if (currentPosSection) {
// Add indentation for examples
currentPosSection.content.push(` "${ex}"`);
}
}
else if (clean.startsWith('[ETYMOLOGY]')) {
etymology = clean.replace('[ETYMOLOGY]', '').trim();
}
}
// Assemble content
// eslint-disable-next-line @typescript-eslint/no-explicit-any
result.content = posSections;
if (etymology) {
result.content.push({
content: [etymology],
style: 'dim',
title: 'Etymology'
});
}
return result;
}
}

@@ -1,8 +0,5 @@

import { PromptStrategy } from '../interfaces/prompt-strategy.js';
import { TranslationResult } from '../providers/types.js';
export declare class TranslationPromptStrategy implements PromptStrategy<TranslationResult> {
import { BaseDictionaryStrategy } from './base-dictionary.js';
export declare class TranslationPromptStrategy extends BaseDictionaryStrategy {
id: string;
getInitialState(): TranslationResult;
getSystemMessage(targetLang: string): string;
parse(text: string): TranslationResult;
}

@@ -1,19 +0,8 @@

export class TranslationPromptStrategy {
import { BaseDictionaryStrategy } from './base-dictionary.js';
export class TranslationPromptStrategy extends BaseDictionaryStrategy {
id = 'translation';
getInitialState() {
return {
antonyms: [],
definitions: [],
examples: [],
idioms: [],
meaning: [],
phonetic: '',
synonyms: [],
tags: [],
word: ''
};
}
getSystemMessage(targetLang) {
return `
You are a professional linguist and translation tool. Analyze the user input.
You are a Senior Lexicographer and professional translation engine.
Your task is to provide a comprehensive dictionary entry for the user input.
Translate the input into "${targetLang}".

@@ -25,24 +14,25 @@

- Format each field with a tag like [TAG] Content.
- Provide BILINGUAL content where appropriate.
- Provide BILINGUAL content where appropriate (Source + Target).
- STRICTLY FOLLOW THE STRUCTURE BELOW.
Required Tags:
[SCHEMA] LAP/1.0
[MODE] translation
[WORD] The original source word ONLY
[PHONETIC] IPA phonetic transcription
[TAG] Domain tags (e.g. Computing, Medical, Slang) - One per line
[POS] Part of Speech (noun, verb, adj, etc.) - Starts a new definition block
[MEANING] Definition in target language (with source term in brackets if helpful)
[EXAMPLE] Example sentence - Must follow the relevant [POS] and [MEANING]
[SYNONYM] Synonyms - One per line
[ANTONYM] Antonyms - One per line
[IDIOM] Common idioms/phrases containing the word - One per line
[ORIGIN] Etymology/Origin of the word (brief)
[PHONETIC] IPA phonetic transcription (UK/US if different)
[TAG] Multi-dimensional tags: Field (e.g. Med), Register (e.g. Formal), Region (e.g. UK), Sentiment (e.g. Pejorative). One per line.
Structure:
- Always start with [WORD] and [PHONETIC].
- Then [TAG]s.
- Then iterate through meanings, grouping them by [POS].
- Inside each [POS], provide the [MEANING] followed immediately by relevant [EXAMPLE]s.
- End with [SYNONYM], [ANTONYM], [IDIOM], [ORIGIN].
[BLOCK:DEF]
[POS] Part of Speech (noun, verb, adj, etc.)
[MEANING] Precise definition in target language. Order definitions by frequency.
[EXAMPLE] Bilingual example sentence showing common collocations.
[END]
[SYNONYM] Synonyms with nuance - One per line (e.g. "hi (嗨) - Informal")
[ANTONYM] Antonyms with nuance - One per line (e.g. "bad (坏) - General")
[IDIOM] Common idioms/phrases containing the word - One per line (e.g. "say hello to (向...问好)")
[ORIGIN] Rich etymology: Trace root -> development -> first use. (e.g. "From Old English hāl (whole) -> Middle English...")
Example Output:
[SCHEMA] LAP/1.0
[MODE] translation
[WORD] hello

@@ -52,67 +42,23 @@ [PHONETIC] /hɛˈloʊ/

[TAG] Informal
[TAG] Interjection
[BLOCK:DEF]
[POS] interjection
[MEANING] 你好 (Used as a greeting or to begin a telephone conversation)
[EXAMPLE] Hello, Paul. (你好,保罗。)
[END]
[BLOCK:DEF]
[POS] noun
[MEANING] 招呼 (An utterance of 'hello'; a greeting)
[EXAMPLE] She said hello to me. (她向我打了个招呼。)
[SYNONYM] hi
[ANTONYM] goodbye
[END]
[SYNONYM] hi (嗨) - Casual
[SYNONYM] greetings (问候) - Formal
[ANTONYM] goodbye (再见) - General
[IDIOM] say hello to (向...问好)
[ORIGIN] Mid 19th century: alteration of hollo.
[ORIGIN] Mid 19th century: alteration of hollo; related to Old High German halā (hail).
`;
}
parse(text) {
const result = this.getInitialState();
const lines = text.split('\n');
let currentDefinition = null;
for (const line of lines) {
const cleanLine = line.trim();
if (!cleanLine)
continue;
if (cleanLine.startsWith('[WORD]')) {
result.word = cleanLine.replace('[WORD]', '').trim();
}
else if (cleanLine.startsWith('[PHONETIC]')) {
result.phonetic = cleanLine.replace('[PHONETIC]', '').trim();
}
else if (cleanLine.startsWith('[POS]')) {
const pos = cleanLine.replace('[POS]', '').trim();
currentDefinition = { examples: [], meaning: '', partOfSpeech: pos };
result.definitions.push(currentDefinition);
}
else if (cleanLine.startsWith('[MEANING]')) {
const meaning = cleanLine.replace('[MEANING]', '').trim();
if (currentDefinition) {
currentDefinition.meaning = meaning;
}
// Always add to flat list for compatibility
result.meaning.push(meaning);
}
else if (cleanLine.startsWith('[EXAMPLE]')) {
const example = cleanLine.replace('[EXAMPLE]', '').trim();
if (currentDefinition) {
currentDefinition.examples.push(example);
}
// Always add to flat list for compatibility
result.examples.push(example);
}
else if (cleanLine.startsWith('[SYNONYM]')) {
result.synonyms.push(cleanLine.replace('[SYNONYM]', '').trim());
}
else if (cleanLine.startsWith('[ANTONYM]')) {
result.antonyms.push(cleanLine.replace('[ANTONYM]', '').trim());
}
else if (cleanLine.startsWith('[IDIOM]')) {
result.idioms.push(cleanLine.replace('[IDIOM]', '').trim());
}
else if (cleanLine.startsWith('[TAG]')) {
result.tags.push(cleanLine.replace('[TAG]', '').trim());
}
else if (cleanLine.startsWith('[ORIGIN]')) {
result.origin = cleanLine.replace('[ORIGIN]', '').trim();
}
}
return result;
}
}

@@ -6,2 +6,6 @@ export interface Definition {

}
export interface TranslationItem {
type: 'antonym' | 'definition' | 'idiom' | 'origin' | 'synonym';
value: Definition | string;
}
export interface TranslationResult {

@@ -12,2 +16,3 @@ antonyms: string[];

idioms: string[];
items?: TranslationItem[];
meaning: string[];

@@ -20,10 +25,13 @@ origin?: string;

}
export type LineStyle = 'bold' | 'code' | 'cyan-bold' | 'dim' | 'green' | 'magenta-bold' | 'normal' | 'yellow';
export interface StyledLine {
style?: LineStyle;
text: string;
}
export interface BoxSection {
content: string | string[];
style?: 'code' | 'dim' | 'normal';
title?: string;
content: (string | StyledLine)[];
title?: string | StyledLine;
}
export interface StandardBoxData {
content: BoxSection[];
footer?: string[];
subtitle?: string;

@@ -30,0 +38,0 @@ tags?: string[];

import { RenderStrategy } from '../interfaces/render-strategy.js';
import { StandardBoxData, TranslationResult } from '../providers/types.js';
export declare class BoxRenderStrategy implements RenderStrategy<StandardBoxData | TranslationResult> {
import { StandardBoxData } from '../providers/types.js';
export declare class BoxRenderStrategy implements RenderStrategy {
id: string;
private lastHeight;
render(result: StandardBoxData | TranslationResult, fromCache: boolean): void;
render(data: StandardBoxData, fromCache: boolean): void;
reset(): void;
private isStandardBoxData;
private normalize;
private normalizeTranslationResult;
}
import boxen from 'boxen';
import chalk from 'chalk';
import wrap from 'wrap-ansi';
const PADDING = 1;
const BORDER_WIDTH = 1;
const TOTAL_HORIZONTAL_MARGIN = (PADDING + BORDER_WIDTH) * 2;
function applyStyle(line) {
if (typeof line === 'string') {
return line;
}
switch (line.style) {
case 'bold': {
return chalk.bold(line.text);
}
case 'code': {
return chalk.gray(line.text);
}
case 'cyan-bold': {
return chalk.cyan.bold(line.text);
}
case 'dim': {
return chalk.dim(line.text);
}
case 'green': {
return chalk.green(line.text);
}
case 'magenta-bold': {
return chalk.magenta.bold(line.text);
}
case 'yellow': {
return chalk.yellow(line.text);
}
default: {
return line.text;
}
}
}
export class BoxRenderStrategy {
id = 'box';
lastHeight = 0;
render(result, fromCache) {
const data = this.normalize(result);
render(data, fromCache) {
const termWidth = process.stdout.columns || 80;
const contentWidth = termWidth - TOTAL_HORIZONTAL_MARGIN;
const wrapContent = (text) => wrap(text, contentWidth, { hard: true, trim: false });
const headerParts = [chalk.bold(data.title || '...')];

@@ -16,30 +53,17 @@ if (data.subtitle)

headerParts.push(chalk.dim('(cached)'));
const header = headerParts.join(' ');
const content = [header, ''];
if (data.content && data.content.length > 0) {
for (const section of data.content) {
if (section.title) {
content.push(chalk.cyan.bold(section.title));
}
const lines = Array.isArray(section.content) ? section.content : [section.content];
for (const line of lines) {
if (section.style === 'dim') {
content.push(chalk.dim(line));
}
else if (section.style === 'code') {
content.push(chalk.gray(line));
}
else {
content.push(line);
}
}
content.push('');
}
}
if (data.footer && data.footer.length > 0) {
if (content.at(-1) !== '')
content.push('');
content.push(...data.footer);
}
const box = boxen(content.join('\n').trim(), { borderColor: fromCache ? 'gray' : 'cyan', borderStyle: 'round', padding: 1 });
const header = wrapContent(headerParts.join(' '));
const sectionContents = (data.content || []).map(section => {
const title = section.title ? applyStyle(section.title) : '';
const content = section.content.map(item => applyStyle(item)).join('\n');
const sectionText = [title, content].filter(Boolean).join('\n');
return wrapContent(sectionText);
});
const finalContent = [header, ...sectionContents]
.filter(Boolean)
.join('\n\n');
const box = boxen(finalContent, {
borderColor: fromCache ? 'gray' : 'cyan',
borderStyle: 'round',
padding: PADDING,
});
if (this.lastHeight > 0) {

@@ -51,4 +75,2 @@ process.stdout.write(`\u001B[${this.lastHeight}A`);

this.lastHeight = box.split('\n').length;
if (fromCache)
this.lastHeight = 0;
}

@@ -58,57 +80,2 @@ reset() {

}
isStandardBoxData(input) {
return 'content' in input && Array.isArray(input.content);
}
normalize(input) {
if (this.isStandardBoxData(input)) {
return input;
}
return this.normalizeTranslationResult(input);
}
normalizeTranslationResult(tr) {
const sections = [];
if (tr.definitions && tr.definitions.length > 0) {
for (const def of tr.definitions) {
const lines = [chalk.green(' • ') + def.meaning];
if (def.examples && def.examples.length > 0) {
for (const ex of def.examples) {
lines.push(chalk.dim(' - ') + ex);
}
}
sections.push({
content: lines,
title: def.partOfSpeech,
});
}
}
else if (tr.meaning && tr.meaning.length > 0) {
const lines = tr.meaning.map(m => chalk.green('• ') + m);
if (tr.examples && tr.examples.length > 0) {
lines.push('', chalk.yellow('Examples:'));
for (const ex of tr.examples)
lines.push(chalk.dim('• ') + ex);
}
sections.push({ content: lines });
}
if (tr.idioms && tr.idioms.length > 0) {
const lines = tr.idioms.map(i => chalk.dim('• ') + i);
sections.push({
content: [chalk.magenta.bold('Idioms:'), ...lines],
});
}
const footer = [];
if (tr.synonyms && tr.synonyms.length > 0)
footer.push(chalk.dim('Synonyms: ') + tr.synonyms.join(', '));
if (tr.antonyms && tr.antonyms.length > 0)
footer.push(chalk.dim('Antonyms: ') + tr.antonyms.join(', '));
if (tr.origin)
footer.push(chalk.dim('Origin: ') + tr.origin);
return {
content: sections,
footer,
subtitle: tr.phonetic,
tags: tr.tags,
title: tr.word,
};
}
}
import { RenderStrategy } from '../interfaces/render-strategy.js';
export declare const RendererFactory: {
get(id: string): RenderStrategy<any>;
get(id: string): RenderStrategy;
};
import { BoxRenderStrategy } from './box.js';
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unused-vars
const strategies = {
'box': new BoxRenderStrategy(),
};
export const RendererFactory = {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
get(id) {
// Create new instance to maintain independent state (like lastHeight)
// Or we could reset state. Creating new is safer.
if (id === 'box')

@@ -12,0 +6,0 @@ return new BoxRenderStrategy();

@@ -25,17 +25,12 @@ import crypto from 'node:crypto';

async play(text, lang = 'en') {
try {
const filePath = await this.getAudioFile(text, lang);
return new Promise((resolve, reject) => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
audioPlayer.play(filePath, (err) => {
if (err)
reject(err);
else
resolve();
});
const filePath = await this.getAudioFile(text, lang);
return new Promise((resolve, reject) => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
audioPlayer.play(filePath, (err) => {
if (err)
reject(err);
else
resolve();
});
}
catch (error) {
console.error('Failed to play audio:', error);
}
});
}

@@ -53,12 +48,21 @@ async getAudioFile(text, lang) {

const url = `http://translate.google.com/translate_tts?ie=UTF-8&client=tw-ob&q=${encodeURIComponent(text)}&tl=${lang}`;
// eslint-disable-next-line n/no-unsupported-features/node-builtins
const response = await fetch(url);
if (!response.ok) {
throw new Error(`TTS API failed: ${response.statusText}`);
try {
// eslint-disable-next-line n/no-unsupported-features/node-builtins
const response = await fetch(url);
if (!response.ok) {
throw new Error(`TTS API failed: ${response.statusText}`);
}
const arrayBuffer = await response.arrayBuffer();
const buffer = Buffer.from(arrayBuffer);
fs.writeFileSync(filePath, buffer);
return filePath;
}
const arrayBuffer = await response.arrayBuffer();
const buffer = Buffer.from(arrayBuffer);
fs.writeFileSync(filePath, buffer);
return filePath;
catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
if (errorMessage.includes('fetch failed')) {
throw new Error('Network error: Unable to access Google TTS service. Please check your network connection.');
}
throw error;
}
}
}

@@ -1,5 +0,5 @@

import { TranslationResult } from '../providers/types.js';
import { StandardBoxData } from '../providers/types.js';
export interface HistoryEntry {
provider: string;
result: TranslationResult;
result: StandardBoxData;
source: string;

@@ -20,3 +20,3 @@ targetLang: string;

getLatest(): HistoryEntry | undefined;
save(text: string, result: TranslationResult, provider: string, targetLang: string): void;
save(text: string, result: StandardBoxData, provider: string, targetLang: string): void;
}

@@ -200,5 +200,7 @@ import { checkbox, confirm, input as inputPrompt, password, select } from '@inquirer/prompts';

choices: [
{ name: 'Translation', value: 'translation' },
{ name: 'Grammar Check', value: 'grammar' },
{ name: 'Oxford Dictionary', value: 'oxford' }
{ name: 'Translation (通用翻译)', value: 'translation' },
{ name: 'Youdao (有道词典风格)', value: 'youdao' },
{ name: 'Oxford (牛津词典风格)', value: 'oxford' },
{ name: 'Cambridge (剑桥词典风格)', value: 'cambridge' },
{ name: 'Grammar Check (语法检查)', value: 'grammar' }
],

@@ -550,4 +552,2 @@ message: 'Select Mode:',

const mode = configService.getMode();
// Strategies (Hardcoded for now)
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const promptStrategy = PromptFactory.get(mode);

@@ -566,8 +566,15 @@ const renderStrategy = RendererFactory.get('box');

const stream = provider.stream(text, { systemMessage });
let buffer = '';
// Use streaming parser if available
const streamParser = promptStrategy.createStreamParser?.({ targetLang });
let result = promptStrategy.getInitialState();
let lastRender = 0;
for await (const chunk of stream) {
buffer += chunk;
result = promptStrategy.parse(buffer);
if (streamParser) {
streamParser.feed(chunk);
result = streamParser.getResult();
}
else {
// Fallback: accumulate and parse
result = promptStrategy.parse(chunk, { targetLang });
}
const now = Date.now();

@@ -579,9 +586,9 @@ if (now - lastRender > 50) {

}
// Force override word with input text to ensure clean header
if (result && typeof result === 'object' && 'word' in result) {
result.word = text;
// Final result - override title with input text for word lookups
if (result.title === '' || !result.title) {
result.title = text;
}
renderStrategy.render(result, false);
renderStrategy.reset(); // Clean up after done
// 2. Save to History
// Save to History
historyService.save(text, result, configService.config.current, targetLang);

@@ -592,7 +599,2 @@ }

process.stdout.cursorTo(0);
// We pass a dummy spinner or undefined, handleProviderError needs to support that
// But since handleProviderError might use spinner, we should check its signature
// Actually handleProviderError in utils takes spinner?
// Let's assume it handles missing spinner or we pass null.
// In repl we don't have a spinner usually.
if (await handleProviderError(error, configService)) {

@@ -614,7 +616,14 @@ await handleTranslation(text, configService);

}
catch {
catch (error) {
process.stdout.clearLine(0);
process.stdout.cursorTo(0);
console.log(chalk.red('Audio playback failed. System player might be missing (requires afplay/mpg123).'));
const errorMessage = error instanceof Error ? error.message : String(error);
if (errorMessage.includes('Network error')) {
console.log(chalk.red(`✗ ${errorMessage}`));
}
else {
console.log(chalk.red('✗ Audio playback failed. System player might be missing (requires afplay, mpg123).'));
console.log(chalk.dim(` Details: ${errorMessage}`));
}
}
}

@@ -31,3 +31,3 @@ {

},
"version": "1.1.0"
"version": "1.2.0"
}
{
"name": "xtrans",
"description": "Minimalist AI-powered terminal translator / 极简 AI 终端翻译工具",
"version": "1.1.0",
"version": "1.2.0",
"author": "wengqianshan",

@@ -23,2 +23,3 @@ "bin": {

"uuid": "^13.0.0",
"wrap-ansi": "^9.0.2",
"zod": "^4.3.6"

@@ -25,0 +26,0 @@ },