diff --git a/android/app/build.gradle b/android/app/build.gradle index 302abb3031..e098d15f09 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -167,12 +167,12 @@ android { dimension "environment" applicationId "me.tinykitten.trainlcd.dev" versionNameSuffix "-dev" - versionCode 100000779 + versionCode 100000781 versionName "10.15.1" } prod { dimension "environment" - versionCode 100000779 + versionCode 100000781 versionName "10.15.1" } // ストア配信する dev (Canary) と applicationId を分けた、ローカル検証専用のフレーバー。 @@ -183,7 +183,7 @@ android { dimension "environment" applicationId "me.tinykitten.trainlcd.local" versionNameSuffix "-local" - versionCode 100000779 + versionCode 100000781 versionName "10.15.1" } } diff --git a/android/wearable/build.gradle.kts b/android/wearable/build.gradle.kts index f69c3189b4..97533ab159 100644 --- a/android/wearable/build.gradle.kts +++ b/android/wearable/build.gradle.kts @@ -85,12 +85,12 @@ android { dimension = "environment" applicationIdSuffix = ".dev" versionNameSuffix = "-dev" - versionCode = 100000780 + versionCode = 100000782 versionName = "10.15.1" } create("prod") { dimension = "environment" - versionCode = 100000780 + versionCode = 100000782 versionName = "10.15.1" } // :app の local フレーバーと対になるローカル検証専用フレーバー。Wear の Data Layer は @@ -100,7 +100,7 @@ android { dimension = "environment" applicationIdSuffix = ".local" versionNameSuffix = "-local" - versionCode = 100000780 + versionCode = 100000782 versionName = "10.15.1" } } diff --git a/app.config.ts b/app.config.ts index 2a7a40fbfd..63b83c8edd 100644 --- a/app.config.ts +++ b/app.config.ts @@ -57,7 +57,7 @@ export default { userInterfaceStyle: 'automatic', // Expo SDK 57 の各モジュール(expo / expo-modules-core ほか)は podspec で iOS 16.4 以上を要求する deploymentTarget: '16.4', - buildNumber: '2919', + buildNumber: '2920', scheme: IS_DEV ? 'CanaryTrainLCD' : 'ProdTrainLCD', bundleIdentifier: IS_DEV ? 'me.tinykitten.trainlcd.dev' @@ -71,7 +71,7 @@ export default { ? 'me.tinykitten.trainlcd.dev' : 'me.tinykitten.trainlcd', permissions: [], - versionCode: 100000779, + versionCode: 100000781, }, owner: 'trainlcd', experiments: { diff --git a/ios/TrainLCD.xcodeproj/project.pbxproj b/ios/TrainLCD.xcodeproj/project.pbxproj index 799f1e6ec3..8415fc739a 100644 --- a/ios/TrainLCD.xcodeproj/project.pbxproj +++ b/ios/TrainLCD.xcodeproj/project.pbxproj @@ -2562,7 +2562,7 @@ CODE_SIGN_ENTITLEMENTS = ProdTrainLCD.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 2919; + CURRENT_PROJECT_VERSION = 2920; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_TEAM = E6R2G33Z36; INFOPLIST_FILE = TrainLCD/Schemes/Prod/Info.plist; @@ -2602,7 +2602,7 @@ CODE_SIGN_ENTITLEMENTS = ProdTrainLCD.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 2919; + CURRENT_PROJECT_VERSION = 2920; DEVELOPMENT_TEAM = E6R2G33Z36; INFOPLIST_FILE = TrainLCD/Schemes/Prod/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = TrainLCD; @@ -2661,7 +2661,7 @@ CODE_SIGN_ENTITLEMENTS = TrainLCD/trainlcd.entitlements; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 2919; + CURRENT_PROJECT_VERSION = 2920; CXX = "$(REACT_NATIVE_PATH)/scripts/xcode/ccache-clang++.sh"; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; @@ -2768,7 +2768,7 @@ CODE_SIGN_ENTITLEMENTS = TrainLCD/trainlcd.entitlements; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; COPY_PHASE_STRIP = YES; - CURRENT_PROJECT_VERSION = 2919; + CURRENT_PROJECT_VERSION = 2920; CXX = "$(REACT_NATIVE_PATH)/scripts/xcode/ccache-clang++.sh"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; @@ -2849,7 +2849,7 @@ CODE_SIGN_ENTITLEMENTS = CanaryTrainLCD.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 2919; + CURRENT_PROJECT_VERSION = 2920; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_TEAM = E6R2G33Z36; INFOPLIST_FILE = TrainLCD/Schemes/Dev/Info.plist; @@ -2889,7 +2889,7 @@ CODE_SIGN_ENTITLEMENTS = CanaryTrainLCD.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 2919; + CURRENT_PROJECT_VERSION = 2920; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = E6R2G33Z36; INFOPLIST_FILE = TrainLCD/Schemes/Dev/Info.plist; @@ -3101,7 +3101,7 @@ CODE_SIGN_ENTITLEMENTS = RideSessionActivity/CanaryRideSessionActivity.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 2919; + CURRENT_PROJECT_VERSION = 2920; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = E6R2G33Z36; GCC_C_LANGUAGE_STANDARD = gnu11; @@ -3153,7 +3153,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 2919; + CURRENT_PROJECT_VERSION = 2920; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = E6R2G33Z36; GCC_C_LANGUAGE_STANDARD = gnu11; @@ -3204,7 +3204,7 @@ CODE_SIGN_ENTITLEMENTS = WatchWidget/ProdWatchWidget.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 2919; + CURRENT_PROJECT_VERSION = 2920; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = E6R2G33Z36; ENABLE_USER_SCRIPT_SANDBOXING = YES; @@ -3262,7 +3262,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 2919; + CURRENT_PROJECT_VERSION = 2920; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = E6R2G33Z36; ENABLE_USER_SCRIPT_SANDBOXING = YES; @@ -3313,7 +3313,7 @@ CODE_SIGN_ENTITLEMENTS = WatchWidget/CanaryWatchWidget.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 2919; + CURRENT_PROJECT_VERSION = 2920; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = E6R2G33Z36; ENABLE_USER_SCRIPT_SANDBOXING = YES; @@ -3370,7 +3370,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 2919; + CURRENT_PROJECT_VERSION = 2920; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = E6R2G33Z36; ENABLE_USER_SCRIPT_SANDBOXING = YES; @@ -3419,7 +3419,7 @@ CODE_SIGN_ENTITLEMENTS = RideSessionActivity/ProdRideSessionActivity.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 2919; + CURRENT_PROJECT_VERSION = 2920; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = E6R2G33Z36; GCC_C_LANGUAGE_STANDARD = gnu11; @@ -3471,7 +3471,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 2919; + CURRENT_PROJECT_VERSION = 2920; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = E6R2G33Z36; GCC_C_LANGUAGE_STANDARD = gnu11; @@ -3690,7 +3690,7 @@ CODE_SIGN_ENTITLEMENTS = ProdAppClip/ProdAppClip.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 2919; + CURRENT_PROJECT_VERSION = 2920; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = E6R2G33Z36; ENABLE_USER_SCRIPT_SANDBOXING = NO; @@ -3746,7 +3746,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 2919; + CURRENT_PROJECT_VERSION = 2920; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = E6R2G33Z36; ENABLE_USER_SCRIPT_SANDBOXING = NO; @@ -3796,7 +3796,7 @@ CODE_SIGN_ENTITLEMENTS = CanaryAppClip/CanaryAppClip.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 2919; + CURRENT_PROJECT_VERSION = 2920; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = E6R2G33Z36; ENABLE_USER_SCRIPT_SANDBOXING = NO; @@ -3854,7 +3854,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 2919; + CURRENT_PROJECT_VERSION = 2920; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = E6R2G33Z36; ENABLE_USER_SCRIPT_SANDBOXING = NO; diff --git a/src/utils/englishReading.test.ts b/src/utils/englishReading.test.ts index 883626e73f..4b2c195029 100644 --- a/src/utils/englishReading.test.ts +++ b/src/utils/englishReading.test.ts @@ -34,10 +34,43 @@ describe('fixEnglishReading', () => { ); }); + it('「Mine」を英語 TTS が「みね」と読む表記へ置換する', () => { + expect(fixEnglishReading('Change here for the Mine Line.')).toBe( + 'Change here for the Me-nay Line.' + ); + expect(fixEnglishReading('The next station is MINE.')).toBe( + 'The next station is Me-nay.' + ); + expect(fixEnglishReading('The next station is Nishi-Mine.')).toBe( + 'The next station is Nishi-Me-nay.' + ); + }); + + it('語中の「mine」も前後にハイフンを挟んで置換する', () => { + expect(fixEnglishReading('The next station is Tsurugamine.')).toBe( + 'The next station is Tsuruga-me-nay.' + ); + expect(fixEnglishReading('Mitsumineguchi')).toBe('Mitsu-me-nay-guchi'); + expect(fixEnglishReading('Minenobu')).toBe('Me-nay-nobu'); + expect(fixEnglishReading('Takamine')).toBe('Taka-me-nay'); + // マクロン付きの文字も語の一部として扱う + expect(fixEnglishReading('Minami-Ōmine')).toBe('Minami-Ō-me-nay'); + }); + + it('二重に適用しても結果が変わらない', () => { + expect(fixEnglishReading(fixEnglishReading('Mine Line'))).toBe( + 'Me-nay Line' + ); + expect(fixEnglishReading(fixEnglishReading('Tsurugamine'))).toBe( + 'Tsuruga-me-nay' + ); + }); + it('別語の一部は置換しない', () => { expect(fixEnglishReading('Keiseibus')).toBe('Keiseibus'); // 西武園 (Seibuen) は 1 語なので語単位の一致では対象外 expect(fixEnglishReading('Seibuen')).toBe('Seibuen'); + expect(fixEnglishReading('Minami-Urawa')).toBe('Minami-Urawa'); }); it('対象を含まないテキストはそのまま返す', () => { diff --git a/src/utils/englishReading.ts b/src/utils/englishReading.ts index be492c9327..4abbd5882d 100644 --- a/src/utils/englishReading.ts +++ b/src/utils/englishReading.ts @@ -5,10 +5,28 @@ // 表示用テキストには適用しないこと (TTS 生成時専用)。 type EnglishReadingRule = { - // 置換対象。ASCII 英字の単語境界 (`\b`) で囲み、`Keisei-Ueno` のような - // ハイフン連結の駅名でも語単位で一致させる。 + // 置換対象。原則は ASCII 英字の単語境界 (`\b`) で囲み、`Keisei-Ueno` のような + // ハイフン連結の駅名でも語単位で一致させる。語中も置換する場合 (Mine) は + // reading を関数にして、前後の文字との区切りを補う。 pattern: RegExp; - reading: string; + reading: string | ((match: string, offset: number, text: string) => string); +}; + +// ローマ字表記の文字か。端末内蔵 TTS 向けの経路ではマクロンを除去しないため、 +// 「Ō」などのラテン拡張文字も語の一部として扱う。 +const isRomanLetter = (char: string | undefined): boolean => + char !== undefined && /[A-Za-z\u00C0-\u024F]/.test(char); + +// 「mine」を読み替え先へ置換する。Tsurugamine のように語中にある場合も置換し、 +// 前後の文字とつながって別の綴りとして読まれないよう、接する側にハイフンを挟む。 +// 先頭の大文字・小文字は元の綴りに合わせる (Mine → Me-nay, Takamine → Taka-me-nay)。 +// TrainLCD/functions の normalizeRomanText (replaceMine) と同じ置換にして、 +// リモート TTS が無効な回とも読みを揃える。 +const toMineReading = (match: string, offset: number, text: string) => { + const before = isRomanLetter(text[offset - 1]) ? '-' : ''; + const after = isRomanLetter(text[offset + match.length]) ? '-' : ''; + const head = /[A-Z]/.test(match.charAt(0)) ? 'Me' : 'me'; + return `${before}${head}-nay${after}`; }; // NOTE: 読み替え先は必ず英語の辞書語 (か、辞書語のハイフン連結) にする。 @@ -20,6 +38,10 @@ const ENGLISH_READING_RULES: readonly EnglishReadingRule[] = [ // 「Seibu (西武)」も同じく "ei" を /aɪ/ と推定して「さいぶ」と読むため、 // "Say" + "boo" で /seɪ.buː/ (せいぶ) を確定させる。 { pattern: /\bSeibu\b/gi, reading: 'Say-boo' }, + // 「Mine (美祢・峰など)」は英語 TTS が英単語 "mine" (まいん) として読むため、 + // "Me" + "nay" で /mi.neɪ/ (みね) に寄せる。Tsurugamine・Mitsumineguchi の + // ように語中にある場合も対象にするため、単語境界では区切らない。 + { pattern: /mine/gi, reading: toMineReading }, ]; /** @@ -27,6 +49,9 @@ const ENGLISH_READING_RULES: readonly EnglishReadingRule[] = [ */ export const fixEnglishReading = (text: string): string => ENGLISH_READING_RULES.reduce( - (acc, { pattern, reading }) => acc.replace(pattern, reading), + (acc, { pattern, reading }) => + typeof reading === 'string' + ? acc.replace(pattern, reading) + : acc.replace(pattern, reading), text );