summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSho Sakuma <me@m1sk9.dev>2026-01-20 21:07:38 +0900
committerGitHub <noreply@github.com>2026-01-20 21:07:38 +0900
commit491420e94db9b21d350ee49df45eb129dec960d2 (patch)
tree7828da3e59c677d7778786586bb9c95061b82df5
parent3b1d35deb3488428dfcff0cd8448c0b0ed620903 (diff)
parent061bd455faad138ca1aefeb73be534198a4e0a44 (diff)
downloadLunaticChat-491420e94db9b21d350ee49df45eb129dec960d2.tar.gz
LunaticChat-491420e94db9b21d350ee49df45eb129dec960d2.tar.bz2
LunaticChat-491420e94db9b21d350ee49df45eb129dec960d2.zip
Merge pull request #49 from m1sk9/fix-kana-conversionv0.4.1
fix: Fix Kana conversion with voiced consonants
-rwxr-xr-x.github/scripts/post-test-results.sh101
-rw-r--r--.github/workflows/ci.yaml16
-rw-r--r--CHANGELOG.md4
-rw-r--r--build.gradle.kts14
-rw-r--r--engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/converter/KanaConverter.kt30
-rw-r--r--engine/src/test/kotlin/dev/m1sk9/lunaticChat/engine/converter/KanaConverterTest.kt293
-rw-r--r--platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/LunaticChat.kt29
7 files changed, 470 insertions, 17 deletions
diff --git a/.github/scripts/post-test-results.sh b/.github/scripts/post-test-results.sh
new file mode 100755
index 0000000..f8fbdf2
--- /dev/null
+++ b/.github/scripts/post-test-results.sh
@@ -0,0 +1,101 @@
+#!/bin/bash
+set -e
+
+MARKER="<!-- test-results-comment -->"
+
+TOTAL_TESTS=0
+PASSED_TESTS=0
+FAILED_TESTS=0
+SKIPPED_TESTS=0
+FAILED_TEST_NAMES=""
+
+for xml_file in */build/test-results/test/*.xml; do
+ if [ -f "$xml_file" ]; then
+ tests=$(sed -n 's/.*tests="\([0-9]*\)".*/\1/p' "$xml_file" | head -n1)
+ failures=$(sed -n 's/.*failures="\([0-9]*\)".*/\1/p' "$xml_file" | head -n1)
+ skipped=$(sed -n 's/.*skipped="\([0-9]*\)".*/\1/p' "$xml_file" | head -n1)
+
+ tests=${tests:-0}
+ failures=${failures:-0}
+ skipped=${skipped:-0}
+
+ TOTAL_TESTS=$((TOTAL_TESTS + tests))
+ FAILED_TESTS=$((FAILED_TESTS + failures))
+ SKIPPED_TESTS=$((SKIPPED_TESTS + skipped))
+
+ if [ "$failures" -gt 0 ]; then
+ failed_names=$(sed -n 's/.*<testcase name="\([^"]*\)".*<failure.*/\1/p' "$xml_file" || echo "")
+ if [ -n "$failed_names" ]; then
+ FAILED_TEST_NAMES="${FAILED_TEST_NAMES}${failed_names}\n"
+ fi
+ fi
+ fi
+done
+
+PASSED_TESTS=$((TOTAL_TESTS - FAILED_TESTS - SKIPPED_TESTS))
+
+if [ "$FAILED_TESTS" -gt 0 ]; then
+ STATUS="❌ Failed"
+ STATUS_EMOJI="❌"
+else
+ STATUS="✅ Passed"
+ STATUS_EMOJI="✅"
+fi
+
+COMMENT_BODY="${MARKER}
+## ${STATUS_EMOJI} Test Results
+
+| Status | Count |
+|--------|-------|
+| ✅ Passed | ${PASSED_TESTS} |
+| ❌ Failed | ${FAILED_TESTS} |
+| ⏭️ Skipped | ${SKIPPED_TESTS} |
+| **Total** | **${TOTAL_TESTS}** |
+
+**Overall Status:** ${STATUS}
+"
+
+if [ "$FAILED_TESTS" -gt 0 ] && [ -n "$FAILED_TEST_NAMES" ]; then
+ COMMENT_BODY="${COMMENT_BODY}
+
+<details>
+<summary>❌ Failed Tests</summary>
+
+$(echo -e "$FAILED_TEST_NAMES" | sed 's/^/- /')
+
+</details>
+"
+fi
+
+COMMENT_BODY="${COMMENT_BODY}
+
+---
+*Updated at $(date -u '+%Y-%m-%d %H:%M:%S UTC')*"
+
+echo "$COMMENT_BODY" > /tmp/test-comment.md
+
+PR_NUMBER="${GITHUB_REF#refs/pull/}"
+PR_NUMBER="${PR_NUMBER%/merge}"
+
+if [ -z "$PR_NUMBER" ] || [ "$PR_NUMBER" = "$GITHUB_REF" ]; then
+ echo "Not a pull request, skipping comment"
+ exit 0
+fi
+
+EXISTING_COMMENT_ID=$(gh api \
+ "repos/$GITHUB_REPOSITORY/issues/${PR_NUMBER}/comments" \
+ --jq ".[] | select(.body | contains(\"$MARKER\")) | .id" \
+ | head -n 1 || echo "")
+
+if [ -n "$EXISTING_COMMENT_ID" ]; then
+ echo "Updating existing comment (ID: $EXISTING_COMMENT_ID)"
+ gh api \
+ "repos/$GITHUB_REPOSITORY/issues/comments/${EXISTING_COMMENT_ID}" \
+ -X PATCH \
+ -f body="$(cat /tmp/test-comment.md)"
+else
+ echo "Creating new comment"
+ gh pr comment "$PR_NUMBER" --body-file /tmp/test-comment.md
+fi
+
+echo "Test results comment posted successfully"
diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml
index 987ceb4..6cab034 100644
--- a/.github/workflows/ci.yaml
+++ b/.github/workflows/ci.yaml
@@ -11,6 +11,10 @@ on:
jobs:
build_plugin:
runs-on: ubuntu-24.04
+ permissions:
+ contents: read
+ pull-requests: write
+ issues: write
steps:
- name: Checkout repository
uses: actions/checkout@v6
@@ -24,6 +28,18 @@ jobs:
- name: Check ktlint
run: ./gradlew ktlintCheck --no-daemon
+ - name: Run tests
+ run: ./gradlew test --no-daemon
+ continue-on-error: true
+
+ - name: Post test results to PR
+ if: github.event_name == 'pull_request' && always()
+ env:
+ GH_TOKEN: ${{ github.token }}
+ GITHUB_REPOSITORY: ${{ github.repository }}
+ GITHUB_REF: ${{ github.ref }}
+ run: .github/scripts/post-test-results.sh
+
- name: Build with shadowJar
run: ./gradlew :platform-paper:shadowJar --parallel --no-daemon
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 7d5ffca..2f692a7 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,9 @@
# LunaticChat Changelog
+### v0.4.1
+
+- Fixed Kana conversion with voiced consonants.
+
### v0.4.0
- Added an update checker that verifies whether updates are available when the server starts up.
diff --git a/build.gradle.kts b/build.gradle.kts
index 9430417..3def8c7 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -12,7 +12,7 @@ plugins {
allprojects {
group = "dev.m1sk9"
- version = "0.4.0"
+ version = "0.4.1"
repositories {
mavenCentral()
@@ -28,4 +28,16 @@ subprojects {
jvmTarget.set(JvmTarget.JVM_21)
}
}
+
+ tasks.withType<Test> {
+ useJUnitPlatform()
+ }
+
+ afterEvaluate {
+ dependencies {
+ add("testImplementation", "org.jetbrains.kotlin:kotlin-test-junit5")
+ add("testImplementation", "org.junit.jupiter:junit-jupiter-api:5.11.4")
+ add("testRuntimeOnly", "org.junit.jupiter:junit-jupiter-engine:5.11.4")
+ }
+ }
}
diff --git a/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/converter/KanaConverter.kt b/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/converter/KanaConverter.kt
index 47ab960..1b3c67d 100644
--- a/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/converter/KanaConverter.kt
+++ b/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/converter/KanaConverter.kt
@@ -20,6 +20,20 @@ object KanaConverter {
private fun buildTrie(): TrieNode {
val mappings =
listOf(
+ // 4文字変換 (x/l prefix small characters)
+ "xtsu" to "っ",
+ "ltsu" to "っ",
+ // 3文字変換 (x/l prefix small characters)
+ "xtu" to "っ",
+ "ltu" to "っ",
+ "xya" to "ゃ",
+ "lya" to "ゃ",
+ "xyu" to "ゅ",
+ "lyu" to "ゅ",
+ "xyo" to "ょ",
+ "lyo" to "ょ",
+ "xwa" to "ゎ",
+ "lwa" to "ゎ",
// 3文字変換
"kya" to "きゃ",
"kyi" to "きぃ",
@@ -104,6 +118,17 @@ object KanaConverter {
"whi" to "うぃ",
"whe" to "うぇ",
"who" to "うぉ",
+ // 2文字変換 (x/l prefix small vowels)
+ "xa" to "ぁ",
+ "la" to "ぁ",
+ "xi" to "ぃ",
+ "li" to "ぃ",
+ "xu" to "ぅ",
+ "lu" to "ぅ",
+ "xe" to "ぇ",
+ "le" to "ぇ",
+ "xo" to "ぉ",
+ "lo" to "ぉ",
// 2文字変換
"ka" to "か",
"ki" to "き",
@@ -181,11 +206,6 @@ object KanaConverter {
"wu" to "う",
"we" to "ゑ",
"wo" to "を",
- "la" to "ら",
- "li" to "り",
- "lu" to "る",
- "le" to "れ",
- "lo" to "ろ",
"nn" to "ん",
// 1文字変換
"a" to "あ",
diff --git a/engine/src/test/kotlin/dev/m1sk9/lunaticChat/engine/converter/KanaConverterTest.kt b/engine/src/test/kotlin/dev/m1sk9/lunaticChat/engine/converter/KanaConverterTest.kt
new file mode 100644
index 0000000..a348fa3
--- /dev/null
+++ b/engine/src/test/kotlin/dev/m1sk9/lunaticChat/engine/converter/KanaConverterTest.kt
@@ -0,0 +1,293 @@
+package dev.m1sk9.lunaticChat.engine.converter
+
+import kotlin.test.Test
+import kotlin.test.assertEquals
+
+class KanaConverterTest {
+ @Test
+ fun `should convert single vowels to hiragana`() {
+ assertEquals("あ", KanaConverter.toHiragana("a"))
+ assertEquals("い", KanaConverter.toHiragana("i"))
+ assertEquals("う", KanaConverter.toHiragana("u"))
+ assertEquals("え", KanaConverter.toHiragana("e"))
+ assertEquals("お", KanaConverter.toHiragana("o"))
+ }
+
+ @Test
+ fun `should convert basic ka-row consonants to hiragana`() {
+ assertEquals("か", KanaConverter.toHiragana("ka"))
+ assertEquals("き", KanaConverter.toHiragana("ki"))
+ assertEquals("く", KanaConverter.toHiragana("ku"))
+ assertEquals("け", KanaConverter.toHiragana("ke"))
+ assertEquals("こ", KanaConverter.toHiragana("ko"))
+ }
+
+ @Test
+ fun `should convert simple word konnichiwa to hiragana`() {
+ assertEquals("こんいちわ", KanaConverter.toHiragana("konnichiwa"))
+ }
+
+ @Test
+ fun `should convert simple word arigatou to hiragana`() {
+ assertEquals("ありがとう", KanaConverter.toHiragana("arigatou"))
+ }
+
+ @Test
+ fun `should convert xtsu and ltsu to small tsu`() {
+ assertEquals("っ", KanaConverter.toHiragana("xtsu"))
+ assertEquals("っ", KanaConverter.toHiragana("ltsu"))
+ }
+
+ @Test
+ fun `should convert xtu and ltu to small tsu`() {
+ assertEquals("っ", KanaConverter.toHiragana("xtu"))
+ assertEquals("っ", KanaConverter.toHiragana("ltu"))
+ }
+
+ @Test
+ fun `should convert xa and la to small a`() {
+ assertEquals("ぁ", KanaConverter.toHiragana("xa"))
+ assertEquals("ぁ", KanaConverter.toHiragana("la"))
+ }
+
+ @Test
+ fun `should convert xi and li to small i`() {
+ assertEquals("ぃ", KanaConverter.toHiragana("xi"))
+ assertEquals("ぃ", KanaConverter.toHiragana("li"))
+ }
+
+ @Test
+ fun `should convert xu and lu to small u`() {
+ assertEquals("ぅ", KanaConverter.toHiragana("xu"))
+ assertEquals("ぅ", KanaConverter.toHiragana("lu"))
+ }
+
+ @Test
+ fun `should convert xe and le to small e`() {
+ assertEquals("ぇ", KanaConverter.toHiragana("xe"))
+ assertEquals("ぇ", KanaConverter.toHiragana("le"))
+ }
+
+ @Test
+ fun `should convert xo and lo to small o`() {
+ assertEquals("ぉ", KanaConverter.toHiragana("xo"))
+ assertEquals("ぉ", KanaConverter.toHiragana("lo"))
+ }
+
+ @Test
+ fun `should convert xya and lya to small ya`() {
+ assertEquals("ゃ", KanaConverter.toHiragana("xya"))
+ assertEquals("ゃ", KanaConverter.toHiragana("lya"))
+ }
+
+ @Test
+ fun `should convert xyu and lyu to small yu`() {
+ assertEquals("ゅ", KanaConverter.toHiragana("xyu"))
+ assertEquals("ゅ", KanaConverter.toHiragana("lyu"))
+ }
+
+ @Test
+ fun `should convert xyo and lyo to small yo`() {
+ assertEquals("ゃ", KanaConverter.toHiragana("xya"))
+ assertEquals("ゃ", KanaConverter.toHiragana("lya"))
+ }
+
+ @Test
+ fun `should convert xwa and lwa to small wa`() {
+ assertEquals("ゎ", KanaConverter.toHiragana("xwa"))
+ assertEquals("ゎ", KanaConverter.toHiragana("lwa"))
+ }
+
+ @Test
+ fun `should convert user example - are kore op xtute raxtuka de shinanai question`() {
+ assertEquals("って", KanaConverter.toHiragana("xtute"))
+ assertEquals("らっか", KanaConverter.toHiragana("raxtuka"))
+
+ val input = "are kore op xtute raxtuka de shinanai ?"
+ val expected = "あれ これ おp って らっか で しなない ?"
+ val actual = KanaConverter.toHiragana(input)
+
+ assertEquals(expected, actual)
+ }
+
+ @Test
+ fun `should convert xtute to tte using x prefix`() {
+ assertEquals("って", KanaConverter.toHiragana("xtute"))
+ }
+
+ @Test
+ fun `should convert raxtuka to rakka with small tsu`() {
+ assertEquals("らっか", KanaConverter.toHiragana("raxtuka"))
+ }
+
+ @Test
+ fun `should convert double consonants to small tsu`() {
+ assertEquals("きって", KanaConverter.toHiragana("kitte"))
+ assertEquals("がっこう", KanaConverter.toHiragana("gakkou"))
+ assertEquals("ずっと", KanaConverter.toHiragana("zutto"))
+ assertEquals("さっぱり", KanaConverter.toHiragana("sappari"))
+ }
+
+ @Test
+ fun `should handle double t as small tsu`() {
+ assertEquals("まった", KanaConverter.toHiragana("matta"))
+ }
+
+ @Test
+ fun `should handle double k as small tsu`() {
+ assertEquals("がっき", KanaConverter.toHiragana("gakki"))
+ }
+
+ @Test
+ fun `should handle double p as small tsu`() {
+ assertEquals("いっぱい", KanaConverter.toHiragana("ippai"))
+ }
+
+ @Test
+ fun `should convert kya-row to hiragana`() {
+ assertEquals("きゃ", KanaConverter.toHiragana("kya"))
+ assertEquals("きゅ", KanaConverter.toHiragana("kyu"))
+ assertEquals("きょ", KanaConverter.toHiragana("kyo"))
+ }
+
+ @Test
+ fun `should convert sha-row to hiragana`() {
+ assertEquals("しゃ", KanaConverter.toHiragana("sha"))
+ assertEquals("しゅ", KanaConverter.toHiragana("shu"))
+ assertEquals("しょ", KanaConverter.toHiragana("sho"))
+ }
+
+ @Test
+ fun `should convert cha-row to hiragana`() {
+ assertEquals("ちゃ", KanaConverter.toHiragana("cha"))
+ assertEquals("ちゅ", KanaConverter.toHiragana("chu"))
+ assertEquals("ちょ", KanaConverter.toHiragana("cho"))
+ }
+
+ @Test
+ fun `should convert nya-row to hiragana`() {
+ assertEquals("にゃ", KanaConverter.toHiragana("nya"))
+ assertEquals("にゅ", KanaConverter.toHiragana("nyu"))
+ assertEquals("にょ", KanaConverter.toHiragana("nyo"))
+ }
+
+ @Test
+ fun `should convert hya-row to hiragana`() {
+ assertEquals("ひゃ", KanaConverter.toHiragana("hya"))
+ assertEquals("ひゅ", KanaConverter.toHiragana("hyu"))
+ assertEquals("ひょ", KanaConverter.toHiragana("hyo"))
+ }
+
+ @Test
+ fun `should convert mya-row to hiragana`() {
+ assertEquals("みゃ", KanaConverter.toHiragana("mya"))
+ assertEquals("みゅ", KanaConverter.toHiragana("myu"))
+ assertEquals("みょ", KanaConverter.toHiragana("myo"))
+ }
+
+ @Test
+ fun `should convert rya-row to hiragana`() {
+ assertEquals("りゃ", KanaConverter.toHiragana("rya"))
+ assertEquals("りゅ", KanaConverter.toHiragana("ryu"))
+ assertEquals("りょ", KanaConverter.toHiragana("ryo"))
+ }
+
+ @Test
+ fun `should convert gya-row to hiragana`() {
+ assertEquals("ぎゃ", KanaConverter.toHiragana("gya"))
+ assertEquals("ぎゅ", KanaConverter.toHiragana("gyu"))
+ assertEquals("ぎょ", KanaConverter.toHiragana("gyo"))
+ }
+
+ @Test
+ fun `should convert ja-row to hiragana`() {
+ assertEquals("じゃ", KanaConverter.toHiragana("ja"))
+ assertEquals("じ", KanaConverter.toHiragana("ji"))
+ assertEquals("じゅ", KanaConverter.toHiragana("ju"))
+ assertEquals("じょ", KanaConverter.toHiragana("jo"))
+ }
+
+ @Test
+ fun `should return empty string for empty input`() {
+ assertEquals("", KanaConverter.toHiragana(""))
+ }
+
+ @Test
+ fun `should pass through non-romanji characters unchanged`() {
+ assertEquals("123", KanaConverter.toHiragana("123"))
+ assertEquals("!", KanaConverter.toHiragana("!"))
+ assertEquals("?", KanaConverter.toHiragana("?"))
+ }
+
+ @Test
+ fun `should handle mixed romanji and non-romanji`() {
+ assertEquals("あ123い", KanaConverter.toHiragana("a123i"))
+ }
+
+ @Test
+ fun `should handle uppercase input by converting to lowercase first`() {
+ assertEquals("か", KanaConverter.toHiragana("KA"))
+ assertEquals("きゃ", KanaConverter.toHiragana("KYA"))
+ }
+
+ @Test
+ fun `should handle mixed case input`() {
+ assertEquals("こんいちわ", KanaConverter.toHiragana("KoNnIcHiWa"))
+ }
+
+ @Test
+ fun `should convert n to n-sound correctly`() {
+ assertEquals("ん", KanaConverter.toHiragana("n"))
+ assertEquals("ん", KanaConverter.toHiragana("nn"))
+ }
+
+ @Test
+ fun `should handle tsu correctly`() {
+ assertEquals("つ", KanaConverter.toHiragana("tsu"))
+ }
+
+ @Test
+ fun `should handle chi and shi correctly`() {
+ assertEquals("ち", KanaConverter.toHiragana("chi"))
+ assertEquals("し", KanaConverter.toHiragana("shi"))
+ }
+
+ @Test
+ fun `should handle fu correctly`() {
+ assertEquals("ふ", KanaConverter.toHiragana("fu"))
+ assertEquals("ふ", KanaConverter.toHiragana("hu"))
+ }
+
+ @Test
+ fun `should handle wo and wa correctly`() {
+ assertEquals("を", KanaConverter.toHiragana("wo"))
+ assertEquals("わ", KanaConverter.toHiragana("wa"))
+ }
+
+ @Test
+ fun `should convert common greeting ohayou`() {
+ assertEquals("おはよう", KanaConverter.toHiragana("ohayou"))
+ }
+
+ @Test
+ fun `should convert common word sumimasen`() {
+ assertEquals("すみません", KanaConverter.toHiragana("sumimasen"))
+ }
+
+ @Test
+ fun `should convert phrase with double consonants`() {
+ assertEquals("がんばって", KanaConverter.toHiragana("ganbatte"))
+ }
+
+ @Test
+ fun `should convert phrase with small ya-row`() {
+ assertEquals("きゃべつ", KanaConverter.toHiragana("kyabetsu"))
+ }
+
+ @Test
+ fun `should handle long phrase with various patterns`() {
+ val input = "watashi wa nihongo wo benkyou shiteimasu"
+ val expected = "わたし わ にほんご を べんきょう しています"
+ assertEquals(expected, KanaConverter.toHiragana(input))
+ }
+}
diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/LunaticChat.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/LunaticChat.kt
index 7ee2fbd..7a7365e 100644
--- a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/LunaticChat.kt
+++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/LunaticChat.kt
@@ -49,6 +49,9 @@ class LunaticChat :
val httpClient = HttpClient(CIO)
+ // Initialize player settings manager (always needed for DM notifications)
+ initializePlayerSettingsManager(configuration)
+
// Initialize features
if (configuration.features.japaneseConversion.enabled) {
initializeJapaneseConversionFeature(configuration, httpClient)
@@ -92,18 +95,10 @@ class LunaticChat :
}
/**
- * Initializes the Japanese conversion feature including:
- * - Player settings management (YAML-based)
- * - Conversion cache
- * - Google IME API client
- * - Romanji converter
- * - Periodic cache saving task
+ * Initializes the player settings manager.
+ * This is always needed for features like DM notifications.
*/
- private fun initializeJapaneseConversionFeature(
- configuration: LunaticChatConfiguration,
- httpClient: HttpClient,
- ) {
- // Initialize player settings
+ private fun initializePlayerSettingsManager(configuration: LunaticChatConfiguration) {
val settingsFile = dataFolder.resolve(configuration.userSettingsFilePath).toPath()
val storage =
YamlPlayerSettingsStorage(
@@ -118,7 +113,19 @@ class LunaticChat :
logger = logger,
)
playerSettingsManager!!.initialize()
+ }
+ /**
+ * Initializes the Japanese conversion feature including:
+ * - Conversion cache
+ * - Google IME API client
+ * - Romanji converter
+ * - Periodic cache saving task
+ */
+ private fun initializeJapaneseConversionFeature(
+ configuration: LunaticChatConfiguration,
+ httpClient: HttpClient,
+ ) {
// Initialize conversion cache
val cache =
ConversionCache(