refactor(language): remove 'auto' recognition language\n\n- Drop 'auto' from settings types and UI options\n- Migrate existing 'auto' values to detected locale\n- Always send explicit language to API\n- Update tests and copy\n- Adjust defaults and first-run init

This commit is contained in:
mssoftjp 2025-08-16 01:15:23 +09:00
parent 4ed761d152
commit 19d7924aab
6 changed files with 61 additions and 75 deletions

View file

@ -98,13 +98,11 @@ export class TranscriptionService implements ITranscriptionProvider {
formData.append('response_format', 'json');
formData.append('temperature', String(API_CONSTANTS.PARAMETERS.TRANSCRIPTION_TEMPERATURE)); // Deterministic output
// Language setting
if (language !== 'auto') {
formData.append('language', language);
}
// Language setting (auto 廃止のため常に明示指定)
formData.append('language', language);
const prompt = this.buildTranscriptionPrompt(language);
if (language !== 'auto' && prompt) {
if (prompt) {
formData.append('prompt', prompt);
}
@ -229,10 +227,7 @@ export class TranscriptionService implements ITranscriptionProvider {
* Build prompt for GPT-4o transcription (for all languages except auto)
*/
private buildTranscriptionPrompt(language: string): string {
// No prompt for auto language mode (as it might interfere with language detection)
if (language === 'auto') {
return '';
}
// auto は廃止済み
const normalizedLang = this.normalizeLanguage(language);
@ -361,7 +356,7 @@ ${PROMPT_CONSTANTS.KOREAN.SPEAKER_ONLY}
* Normalize language code for consistent processing
*/
private normalizeLanguage(language: string): string {
if (language === 'auto') return 'auto';
// auto は廃止済み
const lang = language.toLowerCase();
if (lang.startsWith('ja')) return 'ja';
if (lang.startsWith('zh')) return 'zh';

View file

@ -159,7 +159,7 @@ export const en: TranslationResource = {
transcriptionLanguage: 'Transcription Language',
transcriptionLanguageDesc: 'Language for voice recognition and transcription.',
pluginLanguage: 'Plugin Language',
pluginLanguageDesc: 'Set language for UI display (voice recognition language is auto-detected separately)',
pluginLanguageDesc: 'Set language for UI display',
// Advanced settings
languageLinking: 'Link UI and recognition languages',
languageLinkingDesc: 'When enabled, recognition language follows UI language. When disabled, you can set recognition language independently.',

View file

@ -9,7 +9,7 @@ export interface VoiceInputSettings {
// 録音設定
maxRecordingSeconds: number; // 最大録音時間(秒)
// 言語設定
transcriptionLanguage: 'auto' | 'ja' | 'en' | 'zh' | 'ko'; // 音声認識言語(後方互換性のため維持)
transcriptionLanguage: 'ja' | 'en' | 'zh' | 'ko'; // 音声認識言語
pluginLanguage: Locale; // プラグインUI表示の言語
customDictionary: SimpleCorrectionDictionary;
// デバッグ設定
@ -18,7 +18,7 @@ export interface VoiceInputSettings {
// 高度設定
advanced: {
languageLinkingEnabled: boolean; // UI言語と認識言語を連動するデフォルト: true
transcriptionLanguage?: 'auto' | 'ja' | 'en' | 'zh' | 'ko'; // 独立した音声認識言語設定
transcriptionLanguage?: 'ja' | 'en' | 'zh' | 'ko'; // 独立した音声認識言語設定
};
}
@ -29,7 +29,7 @@ export const DEFAULT_SETTINGS: VoiceInputSettings = {
// 録音設定
maxRecordingSeconds: 300, // 5分300秒
// 言語設定
transcriptionLanguage: 'auto', // 音声認識言語のデフォルトは自動検出(後方互換性のため維持
transcriptionLanguage: 'en', // 初期値(実際は起動時に環境ロケールへ移行
pluginLanguage: 'en', // 初期値、実際はObsidianの設定に従う
customDictionary: { definiteCorrections: [] },
// デバッグ設定
@ -37,7 +37,7 @@ export const DEFAULT_SETTINGS: VoiceInputSettings = {
logLevel: LogLevel.INFO, // 通常レベル
// 高度設定
advanced: {
languageLinkingEnabled: true, // デフォルトは連動オン(現行動作維持)
transcriptionLanguage: 'auto' // デフォルトは自動検出
languageLinkingEnabled: true,
transcriptionLanguage: 'en'
}
};

View file

@ -221,14 +221,17 @@ export default class VoiceInputPlugin extends Plugin {
this.logger?.info('Migrating interfaceLanguage to pluginLanguage');
}
// languageからtranscriptionLanguageへの移行
// languageからtranscriptionLanguageへの移行autoは検出ロケールへ変換
if ('language' in data && !('transcriptionLanguage' in data)) {
// 既存のlanguageフィールドをtranscriptionLanguageに移行
const langValue = data.language;
if (langValue === 'auto' || langValue === 'ja' || langValue === 'en' || langValue === 'zh' || langValue === 'ko') {
if (langValue === 'ja' || langValue === 'en' || langValue === 'zh' || langValue === 'ko') {
migratedData.transcriptionLanguage = langValue;
} else if (langValue === 'auto') {
// auto は廃止: 起動環境のロケールへ固定
migratedData.transcriptionLanguage = this.detectPluginLanguage();
} else {
migratedData.transcriptionLanguage = 'auto';
migratedData.transcriptionLanguage = this.detectPluginLanguage();
}
delete migratedData.language;
needsSave = true;
@ -322,12 +325,12 @@ export default class VoiceInputPlugin extends Plugin {
this.logger?.info(`Auto-detected language: ${this.settings.pluginLanguage} (from Obsidian: ${getObsidianLocale(this.app)})`);
}
// 高度設定のマイグレーション
// 高度設定のマイグレーションauto を廃止)
if (!hasSettingsKey(data, 'advanced')) {
// 既存ユーザーには言語連動をデフォルトで有効化(現行動作維持)
this.settings.advanced = {
languageLinkingEnabled: true,
transcriptionLanguage: 'auto'
transcriptionLanguage: this.detectPluginLanguage()
};
needsSave = true;
this.logger?.info('Initialized advanced settings with language linking enabled for backward compatibility');
@ -336,17 +339,25 @@ export default class VoiceInputPlugin extends Plugin {
this.settings.advanced.languageLinkingEnabled = true;
needsSave = true;
this.logger?.info('Added languageLinkingEnabled to existing advanced settings');
} else if (data.advanced) {
// auto からの置換
const adv = data.advanced as { transcriptionLanguage?: string };
if (adv.transcriptionLanguage === 'auto') {
this.settings.advanced.transcriptionLanguage = this.detectPluginLanguage();
needsSave = true;
this.logger?.info('Migrated advanced.transcriptionLanguage from auto to detected locale');
}
}
} else {
// 保存データが存在しない場合(初回起動)
this.settings.pluginLanguage = this.detectPluginLanguage();
this.settings.transcriptionLanguage = 'auto';
this.settings.transcriptionLanguage = this.detectPluginLanguage();
this.settings.advanced = {
languageLinkingEnabled: true,
transcriptionLanguage: 'auto'
transcriptionLanguage: this.detectPluginLanguage()
};
needsSave = true;
this.logger?.info(`First run - auto-detected language: ${this.settings.pluginLanguage}, transcriptionLanguage: auto, advanced settings initialized`);
this.logger?.info(`First run - detected locale: ${this.settings.pluginLanguage}, transcriptionLanguage set to detected locale, advanced settings initialized`);
}
// 必要に応じて設定を保存
@ -382,19 +393,17 @@ export default class VoiceInputPlugin extends Plugin {
/**
*
* : 'auto' API 'auto'
* auto
*/
getResolvedLanguage(): 'auto' | 'ja' | 'zh' | 'ko' | 'en' {
getResolvedLanguage(): 'ja' | 'zh' | 'ko' | 'en' {
// 言語連動が無効な場合: advanced.transcriptionLanguage を優先
if (this.settings.advanced?.languageLinkingEnabled === false) {
const advancedLang = this.settings.advanced.transcriptionLanguage ?? 'auto';
return advancedLang as 'auto' | 'ja' | 'zh' | 'ko' | 'en';
const advancedLang = this.settings.advanced?.transcriptionLanguage;
return (advancedLang ?? this.detectPluginLanguage()) as 'ja' | 'zh' | 'ko' | 'en';
}
// 言語連動が有効(デフォルト)の場合: 通常の transcriptionLanguage を使用
// transcriptionLanguage が 'auto' なら 'auto' のまま返す
const baseLang = this.settings.transcriptionLanguage ?? 'auto';
return baseLang as 'auto' | 'ja' | 'zh' | 'ko' | 'en';
// 言語連動が有効: 通常の transcriptionLanguage を使用
const baseLang = this.settings.transcriptionLanguage;
return (baseLang ?? this.detectPluginLanguage()) as 'ja' | 'zh' | 'ko' | 'en';
}
async saveSettings() {

View file

@ -84,7 +84,7 @@ export class VoiceInputSettingTab extends PluginSettingTab {
if (!this.plugin.settings.advanced) {
this.plugin.settings.advanced = {
languageLinkingEnabled: value,
transcriptionLanguage: 'auto'
transcriptionLanguage: this.plugin.getResolvedLanguage()
};
} else {
this.plugin.settings.advanced.languageLinkingEnabled = value;
@ -100,13 +100,12 @@ export class VoiceInputSettingTab extends PluginSettingTab {
.setName(this.i18n.t('ui.settings.advancedTranscriptionLanguage'))
.setDesc(this.i18n.t('ui.settings.advancedTranscriptionLanguageDesc'))
.addDropdown(dropdown => dropdown
.addOption('auto', this.i18n.t('ui.options.languageAuto'))
.addOption('ja', this.i18n.t('ui.options.languageJa'))
.addOption('en', this.i18n.t('ui.options.languageEn'))
.addOption('zh', this.i18n.t('ui.options.languageZh'))
.addOption('ko', this.i18n.t('ui.options.languageKo'))
.setValue(this.plugin.settings.advanced.transcriptionLanguage ?? 'auto')
.onChange(async (value: 'auto' | 'ja' | 'en' | 'zh' | 'ko') => {
.setValue(this.plugin.settings.advanced.transcriptionLanguage ?? this.plugin.getResolvedLanguage())
.onChange(async (value: 'ja' | 'en' | 'zh' | 'ko') => {
if (!this.plugin.settings.advanced) {
this.plugin.settings.advanced = {
languageLinkingEnabled: false,

View file

@ -21,19 +21,12 @@ function detectPluginLanguage(getObsidianLocaleFn: () => string): 'ja' | 'zh' |
}
function getResolvedLanguage(
pluginLanguage: string,
pluginLanguage: string,
detectFn: () => 'ja' | 'zh' | 'ko' | 'en'
): 'auto' | 'ja' | 'zh' | 'ko' | 'en' {
if (pluginLanguage === 'auto') {
// 仕様変更: 'auto' はそのまま返すAPI に渡す)
return 'auto';
}
): 'ja' | 'zh' | 'ko' | 'en' {
if (!['ja', 'zh', 'ko', 'en'].includes(pluginLanguage)) {
// 不正値は従来通り検出へフォールバック
return detectFn();
}
return pluginLanguage as 'ja' | 'zh' | 'ko' | 'en';
}
@ -99,11 +92,7 @@ describe('Language Detection Logic', () => {
expect(mockDetectFn).toHaveBeenCalledTimes(1);
});
test('should handle auto value (pass-through)', () => {
const result = getResolvedLanguage('auto', mockDetectFn);
expect(result).toBe('auto');
expect(mockDetectFn).not.toHaveBeenCalled();
});
// auto は廃止済みのためテストしない
test('should work with all supported languages', () => {
expect(getResolvedLanguage('ja', mockDetectFn)).toBe('ja');
@ -123,24 +112,18 @@ describe('Language Detection Logic', () => {
// Function to simulate the new advanced language resolution logic
function getResolvedLanguageAdvanced(
transcriptionLanguage: string,
transcriptionLanguage: string | undefined,
advanced: { languageLinkingEnabled?: boolean; transcriptionLanguage?: string } | undefined,
detectFn: () => 'ja' | 'zh' | 'ko' | 'en'
): 'auto' | 'ja' | 'zh' | 'ko' | 'en' {
// 高度設定で言語連動が無効な場合: advanced.transcriptionLanguage を優先
): 'ja' | 'zh' | 'ko' | 'en' {
if (advanced?.languageLinkingEnabled === false) {
const advancedLang = advanced.transcriptionLanguage ?? 'auto';
return (advancedLang as 'auto' | 'ja' | 'zh' | 'ko' | 'en');
const adv = advanced?.transcriptionLanguage;
return (['ja', 'zh', 'ko', 'en'].includes(adv as any) ? adv : detectFn()) as 'ja' | 'zh' | 'ko' | 'en';
}
// 言語連動が有効な場合(デフォルト): 通常の transcriptionLanguage を使用
if (transcriptionLanguage === 'auto') {
return 'auto';
}
return transcriptionLanguage as 'ja' | 'zh' | 'ko' | 'en';
return (['ja', 'zh', 'ko', 'en'].includes(transcriptionLanguage as any) ? transcriptionLanguage : detectFn()) as 'ja' | 'zh' | 'ko' | 'en';
}
test('should return auto when linking is enabled and TL is auto', () => {
test('should use TL when linking enabled; fallback to detection when missing/invalid', () => {
// Default case: advanced.languageLinkingEnabled is true
const advanced = { languageLinkingEnabled: true };
@ -148,9 +131,9 @@ describe('Language Detection Logic', () => {
expect(getResolvedLanguageAdvanced('ja', advanced, mockDetectFn)).toBe('ja');
expect(mockDetectFn).not.toHaveBeenCalled();
// Should pass through 'auto'
expect(getResolvedLanguageAdvanced('auto', advanced, mockDetectFn)).toBe('auto');
expect(mockDetectFn).not.toHaveBeenCalled();
// Missing or invalid TL falls back to detection
expect(getResolvedLanguageAdvanced(undefined, advanced, mockDetectFn)).toBe('ko');
expect(getResolvedLanguageAdvanced('invalid', advanced, mockDetectFn)).toBe('ko');
});
test('should treat undefined advanced as linking enabled', () => {
@ -158,8 +141,8 @@ describe('Language Detection Logic', () => {
expect(getResolvedLanguageAdvanced('ja', undefined, mockDetectFn)).toBe('ja');
expect(mockDetectFn).not.toHaveBeenCalled();
expect(getResolvedLanguageAdvanced('auto', undefined, mockDetectFn)).toBe('auto');
expect(mockDetectFn).not.toHaveBeenCalled();
expect(getResolvedLanguageAdvanced(undefined, undefined, mockDetectFn)).toBe('ko');
expect(mockDetectFn).toHaveBeenCalledTimes(1);
});
test('should use advanced.transcriptionLanguage when language linking is disabled', () => {
@ -173,24 +156,24 @@ describe('Language Detection Logic', () => {
expect(mockDetectFn).not.toHaveBeenCalled();
});
test('should pass through auto when linking disabled and advanced TL is auto', () => {
test('should use advanced TL when linking disabled; fallback to detection when missing/invalid', () => {
const advanced = {
languageLinkingEnabled: false,
transcriptionLanguage: 'auto'
transcriptionLanguage: undefined as unknown as string
};
expect(getResolvedLanguageAdvanced('ja', advanced, mockDetectFn)).toBe('auto');
expect(mockDetectFn).not.toHaveBeenCalled();
expect(getResolvedLanguageAdvanced('ja', advanced, mockDetectFn)).toBe('ko');
expect(mockDetectFn).toHaveBeenCalled();
});
test('should pass through auto when linking disabled and advanced TL is undefined', () => {
test('should use detection when linking disabled and advanced TL is undefined', () => {
const advanced = {
languageLinkingEnabled: false
// transcriptionLanguage is undefined
};
expect(getResolvedLanguageAdvanced('ja', advanced, mockDetectFn)).toBe('auto');
expect(mockDetectFn).not.toHaveBeenCalled();
expect(getResolvedLanguageAdvanced('ja', advanced, mockDetectFn)).toBe('ko');
expect(mockDetectFn).toHaveBeenCalled();
});
test('should work with all supported languages in advanced mode', () => {