Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
135 changes: 135 additions & 0 deletions Core/Sources/Core/InputUtils/KatakanaEnglishCandidates.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
/// カタカナで入力した外来語から、対応する英単語の綴りを候補に追加する。
///
/// 機械的なローマ字化ではなく、一般的な英語表記だけを明示的に登録する。
enum KatakanaEnglishCandidates {
private static let words: [String: String] = [
// 日常で使う物・場所
"ベッド": "bed",
"ボトル": "bottle",
"カメラ": "camera",
"カード": "card",
"コーヒー": "coffee",
"ドア": "door",
"ドレス": "dress",
"エレベーター": "elevator",
"フード": "food",
"ホテル": "hotel",
"アイスクリーム": "ice cream",
"ジャケット": "jacket",
"キッチン": "kitchen",
"ランチ": "lunch",
"マップ": "map",
"メニュー": "menu",
"ミラー": "mirror",
"オフィス": "office",
"パスポート": "passport",
"ペン": "pen",
"ピザ": "pizza",
"レストラン": "restaurant",
"シャツ": "shirt",
"ショップ": "shop",
"ソファ": "sofa",
"スーツ": "suit",
"タクシー": "taxi",
"チケット": "ticket",
"トイレ": "toilet",
"トレイン": "train",
"バス": "bus",
"ウォーター": "water",
"ウィンドウ": "window",

// デジタル作業
"アクセス": "access",
"アカウント": "account",
"アプリ": "app",
"アップデート": "update",
"アップロード": "upload",
"ブラウザ": "browser",
"クリック": "click",
"コード": "code",
"コンテンツ": "content",
"データ": "data",
"デバッグ": "debug",
"デバイス": "device",
"ディスプレイ": "display",
"ドキュメント": "document",
"ダウンロード": "download",
"エラー": "error",
"ファイル": "file",
"フォルダ": "folder",
"インストール": "install",
"インターフェース": "interface",
"インターネット": "internet",
"キーボード": "keyboard",
"リンク": "link",
"ログイン": "login",
"マウス": "mouse",
"ネットワーク": "network",
"オプション": "option",
"パスワード": "password",
"プライバシー": "privacy",
"プログラム": "program",
"プロジェクト": "project",
"リリース": "release",
"セキュリティ": "security",
"サーバー": "server",
"サービス": "service",
"ソフトウェア": "software",
"スマートフォン": "smartphone",
"システム": "system",
"タブ": "tab",
"テキスト": "text",
"ユーザー": "user",
"バージョン": "version",
"ウェブ": "web",
"ウェブサイト": "website",

// 仕事・コミュニケーション
"アジェンダ": "agenda",
"アポイントメント": "appointment",
"ミーティング": "meeting",
"メッセージ": "message",
"メール": "email",
"プレゼンテーション": "presentation",
"レポート": "report",
"スケジュール": "schedule",
"タスク": "task",
"チーム": "team",

// よく使う一般語
"アイデア": "idea",
"アイコン": "icon",
"イメージ": "image",
"イベント": "event",
"ゲーム": "game",
"グループ": "group",
"キーワード": "keyword",
"ラベル": "label",
"レイアウト": "layout",
"リスト": "list",
"モデル": "model",
"ニュース": "news",
"ページ": "page",
"パターン": "pattern",
"プラン": "plan",
"ポイント": "point",
"ポリシー": "policy",
"レスポンス": "response",
"ルール": "rule",
"サンプル": "sample",
"サイズ": "size",
"スタイル": "style",
"タイトル": "title",
"トピック": "topic",
"タイプ": "type",
"ワーク": "work"
]

static func variants(for reading: String) -> [String] {
guard let word = Self.words[reading] else {
return []
}
let title = word.prefix(1).uppercased() + word.dropFirst()
return title == word ? [word] : [word, title]
}
}
23 changes: 22 additions & 1 deletion Core/Sources/Core/InputUtils/SegmentsManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -564,7 +564,7 @@ public final class SegmentsManager {

let leftSideContext = forcedLeftSideContext ?? self.getCleanLeftSideContext(maxCount: ContextLength.conversion)
let rightSideContext = forcedRightSideContext ?? self.getCleanRightSideContext(maxCount: ContextLength.conversion)
let result = self.kanaKanjiConverter.requestCandidates(
var result = self.kanaKanjiConverter.requestCandidates(
self.composingText,
options: options(
leftSideContext: leftSideContext,
Expand All @@ -574,6 +574,27 @@ public final class SegmentsManager {
requireEnglishPrediction: Config.DebugPredictiveTyping().value ? .manualMix : .disabled
)
)
if self.composingText.isAtEndIndex {
let reading = self.composingText.convertTarget.toKatakana()
let englishWords = KatakanaEnglishCandidates.variants(for: reading)
if !englishWords.isEmpty {
var seen = Set(result.mainResults.map(\.text))
let candidates = englishWords.compactMap { word -> Candidate? in
guard seen.insert(word).inserted else {
return nil
}
return Candidate(
text: word,
value: -18,
composingCount: .surfaceCount(self.composingText.convertTarget.count),
lastMid: MIDData.一般.mid,
data: [.init(word: word, ruby: reading, cid: CIDData.固有名詞.cid, mid: MIDData.一般.mid, value: -18)],
isLearningTarget: false
)
}
result.mainResults.insert(contentsOf: candidates, at: min(5, result.mainResults.count))
}
}
Comment on lines +577 to +597
self.rawCandidates = result
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
@testable import Core
import Foundation
import KanaKanjiConverterModuleWithDefaultDictionary
import Testing

struct KatakanaEnglishCandidatesTests {
@Test func returnsLowercaseAndSentenceCaseVariants() {
#expect(KatakanaEnglishCandidates.variants(for: "セキュリティ") == ["security", "Security"])
#expect(KatakanaEnglishCandidates.variants(for: "ディスプレイ") == ["display", "Display"])
#expect(KatakanaEnglishCandidates.variants(for: "スマートフォン") == ["smartphone", "Smartphone"])
#expect(KatakanaEnglishCandidates.variants(for: "ベッド") == ["bed", "Bed"])
#expect(KatakanaEnglishCandidates.variants(for: "マウス") == ["mouse", "Mouse"])
#expect(KatakanaEnglishCandidates.variants(for: "キーボード") == ["keyboard", "Keyboard"])
#expect(KatakanaEnglishCandidates.variants(for: "メール") == ["email", "Email"])
}

@Test func doesNotGuessUnknownKatakanaWords() {
#expect(KatakanaEnglishCandidates.variants(for: "アズーキー").isEmpty)
}

@MainActor @Test func insertsEnglishSpellingIntoConversionCandidates() {
let manager = SegmentsManager(
kanaKanjiConverter: .withDefaultDictionary(),
applicationDirectoryURL: URL(fileURLWithPath: NSTemporaryDirectory(), isDirectory: true),
containerURL: nil,
context: .init(useZenzai: false)
)
manager.insertAtCursorPosition("セキュリティ", inputStyle: .direct)
manager.requestSetCandidateWindowState(visible: true)

switch manager.getCurrentCandidateWindow(inputState: .selecting) {
case .selecting(let candidates, _):
let texts = candidates.map(\.text)
#expect(texts.contains("security"))
#expect(texts.contains("Security"))
case .hidden, .composing:
Issue.record("Expected conversion candidates.")
}
}
}