diff --git a/McBopomofo.xcodeproj/project.pbxproj b/McBopomofo.xcodeproj/project.pbxproj index ec38f092..5f4ba050 100644 --- a/McBopomofo.xcodeproj/project.pbxproj +++ b/McBopomofo.xcodeproj/project.pbxproj @@ -11,8 +11,6 @@ 6A0D4F0915FC0DA600ABF4B3 /* Bopomofo@2x.tiff in Resources */ = {isa = PBXBuildFile; fileRef = 6A0D4EF015FC0DA600ABF4B3 /* Bopomofo@2x.tiff */; }; 6A0D4F4515FC0EB100ABF4B3 /* Mandarin.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 6A0D4F2015FC0EB100ABF4B3 /* Mandarin.cpp */; }; 6A0D4F5315FC0EE100ABF4B3 /* preferences.xib in Resources */ = {isa = PBXBuildFile; fileRef = 6A0D4F4E15FC0EE100ABF4B3 /* preferences.xib */; }; - 6A0D4F5715FC0EF900ABF4B3 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 6A0D4F4815FC0EE100ABF4B3 /* InfoPlist.strings */; }; - 6A0D4F5815FC0EF900ABF4B3 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 6A0D4F4A15FC0EE100ABF4B3 /* Localizable.strings */; }; 6A187E2616004C5900466B2E /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 6A187E2816004C5900466B2E /* MainMenu.xib */; }; 6A225A1F23679F2600F685C6 /* NotarizedArchives in Resources */ = {isa = PBXBuildFile; fileRef = 6A225A1E23679F2600F685C6 /* NotarizedArchives */; }; 6A2E40F6253A69DA00D1AE1D /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 6A2E40F5253A69DA00D1AE1D /* Images.xcassets */; }; @@ -48,19 +46,22 @@ D44FB74A2791B829003C80A6 /* VXHanConvert in Frameworks */ = {isa = PBXBuildFile; productRef = D44FB7492791B829003C80A6 /* VXHanConvert */; }; D44FB74D2792189A003C80A6 /* PhraseReplacementMap.cpp in Sources */ = {isa = PBXBuildFile; fileRef = D44FB74B2792189A003C80A6 /* PhraseReplacementMap.cpp */; }; D456576E279E4F7B00DF6BC9 /* KeyHandlerInput.swift in Sources */ = {isa = PBXBuildFile; fileRef = D456576D279E4F7B00DF6BC9 /* KeyHandlerInput.swift */; }; + D45EB5C127A9894C00E28B17 /* StringUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = D45EB5BF27A9890C00E28B17 /* StringUtils.swift */; }; D461B792279DAC010070E734 /* InputState.swift in Sources */ = {isa = PBXBuildFile; fileRef = D461B791279DAC010070E734 /* InputState.swift */; }; D47B92C027972AD100458394 /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = D47B92BF27972AC800458394 /* main.swift */; }; D47D73A427A5D43900255A50 /* KeyHandlerBopomofoTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D47D73A327A5D43900255A50 /* KeyHandlerBopomofoTests.swift */; }; - D47D73C327A7200500255A50 /* FSEventStreamHelper in Frameworks */ = {isa = PBXBuildFile; productRef = D47D73C227A7200500255A50 /* FSEventStreamHelper */; }; D47D73A827A6C84F00255A50 /* associated-phrases.cin in Resources */ = {isa = PBXBuildFile; fileRef = D47D73A727A6C84F00255A50 /* associated-phrases.cin */; }; D47D73A927A6C84F00255A50 /* associated-phrases.cin in Resources */ = {isa = PBXBuildFile; fileRef = D47D73A727A6C84F00255A50 /* associated-phrases.cin */; }; D47D73AC27A6CAE600255A50 /* AssociatedPhrases.cpp in Sources */ = {isa = PBXBuildFile; fileRef = D47D73AA27A6CAE600255A50 /* AssociatedPhrases.cpp */; }; + D47D73C327A7200500255A50 /* FSEventStreamHelper in Frameworks */ = {isa = PBXBuildFile; productRef = D47D73C227A7200500255A50 /* FSEventStreamHelper */; }; D47F7DCE278BFB57002F9DD7 /* PreferencesWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D47F7DCD278BFB57002F9DD7 /* PreferencesWindowController.swift */; }; D47F7DD0278C0897002F9DD7 /* NonModalAlertWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D47F7DCF278C0897002F9DD7 /* NonModalAlertWindowController.swift */; }; D47F7DD3278C1263002F9DD7 /* UserOverrideModel.cpp in Sources */ = {isa = PBXBuildFile; fileRef = D47F7DD2278C1263002F9DD7 /* UserOverrideModel.cpp */; }; D485D3B92796A8A000657FF3 /* PreferencesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D485D3B82796A8A000657FF3 /* PreferencesTests.swift */; }; D485D3C02796CE3200657FF3 /* VersionUpdateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D485D3BF2796CE3200657FF3 /* VersionUpdateTests.swift */; }; D4A13D5A27A59F0B003BE359 /* InputMethodController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4A13D5927A59D5C003BE359 /* InputMethodController.swift */; }; + D4E33D8A27A838CF006DB1CF /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = D4E33D8827A838CF006DB1CF /* Localizable.strings */; }; + D4E33D8F27A838F0006DB1CF /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = D4E33D8D27A838F0006DB1CF /* InfoPlist.strings */; }; D4E569DC27A34D0E00AC2CEF /* KeyHandler.mm in Sources */ = {isa = PBXBuildFile; fileRef = D4E569DB27A34CC100AC2CEF /* KeyHandler.mm */; }; D4E569E427A414CB00AC2CEF /* data-plain-bpmf.txt in Resources */ = {isa = PBXBuildFile; fileRef = 6AD7CBC715FE555000691B5B /* data-plain-bpmf.txt */; }; D4E569E527A414CB00AC2CEF /* data.txt in Resources */ = {isa = PBXBuildFile; fileRef = 6A38BBF615FC117A00A8A51F /* data.txt */; }; @@ -145,10 +146,6 @@ 6A0D4F4015FC0EB100ABF4B3 /* OVTextBuffer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OVTextBuffer.h; sourceTree = ""; }; 6A0D4F4115FC0EB100ABF4B3 /* OVUTF8Helper.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OVUTF8Helper.h; sourceTree = ""; }; 6A0D4F4215FC0EB100ABF4B3 /* OVWildcard.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OVWildcard.h; sourceTree = ""; }; - 6A0D4F4915FC0EE100ABF4B3 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = Source/en.lproj/InfoPlist.strings; sourceTree = ""; }; - 6A0D4F4B15FC0EE100ABF4B3 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = Source/en.lproj/Localizable.strings; sourceTree = ""; }; - 6A0D4F5415FC0EF900ABF4B3 /* zh-Hant */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hant"; path = "Source/zh-Hant.lproj/InfoPlist.strings"; sourceTree = ""; }; - 6A0D4F5515FC0EF900ABF4B3 /* zh-Hant */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hant"; path = "Source/zh-Hant.lproj/Localizable.strings"; sourceTree = ""; }; 6A0D4F5615FC0EF900ABF4B3 /* zh-Hant */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = "zh-Hant"; path = "Source/zh-Hant.lproj/preferences.xib"; sourceTree = ""; }; 6A15B32421A51F2300B92CD3 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = ""; }; 6A15B32521A51F2300B92CD3 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = ""; }; @@ -204,13 +201,14 @@ D44FB74B2792189A003C80A6 /* PhraseReplacementMap.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = PhraseReplacementMap.cpp; sourceTree = ""; }; D44FB74C2792189A003C80A6 /* PhraseReplacementMap.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = PhraseReplacementMap.h; sourceTree = ""; }; D456576D279E4F7B00DF6BC9 /* KeyHandlerInput.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyHandlerInput.swift; sourceTree = ""; }; + D45EB5BF27A9890C00E28B17 /* StringUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StringUtils.swift; sourceTree = ""; }; D461B791279DAC010070E734 /* InputState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InputState.swift; sourceTree = ""; }; D47B92BF27972AC800458394 /* main.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = main.swift; sourceTree = ""; }; D47D73A327A5D43900255A50 /* KeyHandlerBopomofoTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyHandlerBopomofoTests.swift; sourceTree = ""; }; - D47D73C027A71FFA00255A50 /* FSEventStreamHelper */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = FSEventStreamHelper; path = Packages/FSEventStreamHelper; sourceTree = ""; }; D47D73A727A6C84F00255A50 /* associated-phrases.cin */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = "associated-phrases.cin"; sourceTree = ""; }; D47D73AA27A6CAE600255A50 /* AssociatedPhrases.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = AssociatedPhrases.cpp; sourceTree = ""; }; D47D73AB27A6CAE600255A50 /* AssociatedPhrases.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AssociatedPhrases.h; sourceTree = ""; }; + D47D73C027A71FFA00255A50 /* FSEventStreamHelper */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = FSEventStreamHelper; path = Packages/FSEventStreamHelper; sourceTree = ""; }; D47F7DCD278BFB57002F9DD7 /* PreferencesWindowController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreferencesWindowController.swift; sourceTree = ""; }; D47F7DCF278C0897002F9DD7 /* NonModalAlertWindowController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NonModalAlertWindowController.swift; sourceTree = ""; }; D47F7DD1278C1263002F9DD7 /* UserOverrideModel.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = UserOverrideModel.h; sourceTree = ""; }; @@ -220,6 +218,12 @@ D485D3BF2796CE3200657FF3 /* VersionUpdateTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VersionUpdateTests.swift; sourceTree = ""; }; D495583A27A5C6C4006ADE1C /* LanguageModelManager+Privates.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "LanguageModelManager+Privates.h"; sourceTree = ""; }; D4A13D5927A59D5C003BE359 /* InputMethodController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InputMethodController.swift; sourceTree = ""; }; + D4E33D8927A838CF006DB1CF /* Base */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = Base; path = Base.lproj/Localizable.strings; sourceTree = ""; }; + D4E33D8B27A838D5006DB1CF /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = ""; }; + D4E33D8C27A838D8006DB1CF /* zh-Hant */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hant"; path = "zh-Hant.lproj/Localizable.strings"; sourceTree = ""; }; + D4E33D8E27A838F0006DB1CF /* Base */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = Base; path = Base.lproj/InfoPlist.strings; sourceTree = ""; }; + D4E33D9027A838F4006DB1CF /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = ""; }; + D4E33D9127A838F7006DB1CF /* zh-Hant */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hant"; path = "zh-Hant.lproj/InfoPlist.strings"; sourceTree = ""; }; D4E569DA27A34CC100AC2CEF /* KeyHandler.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KeyHandler.h; sourceTree = ""; }; D4E569DB27A34CC100AC2CEF /* KeyHandler.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = KeyHandler.mm; sourceTree = ""; }; D4F0BBDE279AF1AF0071253C /* ArchiveUtil.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArchiveUtil.swift; sourceTree = ""; }; @@ -300,6 +304,7 @@ D4E569DB27A34CC100AC2CEF /* KeyHandler.mm */, D456576D279E4F7B00DF6BC9 /* KeyHandlerInput.swift */, D461B791279DAC010070E734 /* InputState.swift */, + D45EB5BF27A9890C00E28B17 /* StringUtils.swift */, D427F76B278CA1BA004A2160 /* AppDelegate.swift */, D44FB74427915555003C80A6 /* Preferences.swift */, D47F7DCF278C0897002F9DD7 /* NonModalAlertWindowController.swift */, @@ -423,8 +428,8 @@ 6AFF97F0253B299E007F1C49 /* NonModalAlertWindowController.xib */, 6A0D4EEE15FC0DA600ABF4B3 /* Images */, 6A0D4EF515FC0DA600ABF4B3 /* McBopomofo-Info.plist */, - 6A0D4F4815FC0EE100ABF4B3 /* InfoPlist.strings */, - 6A0D4F4A15FC0EE100ABF4B3 /* Localizable.strings */, + D4E33D8D27A838F0006DB1CF /* InfoPlist.strings */, + D4E33D8827A838CF006DB1CF /* Localizable.strings */, 6A187E2816004C5900466B2E /* MainMenu.xib */, 6A0D4F4E15FC0EE100ABF4B3 /* preferences.xib */, ); @@ -640,14 +645,14 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( + D4E33D8A27A838CF006DB1CF /* Localizable.strings in Resources */, 6A6ED16E2797650A0012872E /* template-exclude-phrases.txt in Resources */, 6A0D4F0815FC0DA600ABF4B3 /* Bopomofo.tiff in Resources */, 6A0D4F0915FC0DA600ABF4B3 /* Bopomofo@2x.tiff in Resources */, 6A6ED16D2797650A0012872E /* template-exclude-phrases-plain-bpmf.txt in Resources */, 6A0D4F5315FC0EE100ABF4B3 /* preferences.xib in Resources */, - 6A0D4F5715FC0EF900ABF4B3 /* InfoPlist.strings in Resources */, - 6A0D4F5815FC0EF900ABF4B3 /* Localizable.strings in Resources */, 6A2E40F6253A69DA00D1AE1D /* Images.xcassets in Resources */, + D4E33D8F27A838F0006DB1CF /* InfoPlist.strings in Resources */, 6A38BC1515FC117A00A8A51F /* data.txt in Resources */, 6A6ED16B2797650A0012872E /* template-phrases-replacement.txt in Resources */, 6AFF97F2253B299E007F1C49 /* NonModalAlertWindowController.xib in Resources */, @@ -728,6 +733,7 @@ D47F7DD3278C1263002F9DD7 /* UserOverrideModel.cpp in Sources */, 6A0D4F4515FC0EB100ABF4B3 /* Mandarin.cpp in Sources */, 6ACC3D452793701600F1B140 /* ParselessLM.cpp in Sources */, + D45EB5C127A9894C00E28B17 /* StringUtils.swift in Sources */, D41355DE278EA3ED005E5CBD /* UserPhrasesLM.cpp in Sources */, 6ACC3D3F27914F2400F1B140 /* KeyValueBlobReader.cpp in Sources */, D41355D8278D74B5005E5CBD /* LanguageModelManager.mm in Sources */, @@ -775,26 +781,6 @@ /* End PBXTargetDependency section */ /* Begin PBXVariantGroup section */ - 6A0D4F4815FC0EE100ABF4B3 /* InfoPlist.strings */ = { - isa = PBXVariantGroup; - children = ( - 6A0D4F4915FC0EE100ABF4B3 /* en */, - 6A0D4F5415FC0EF900ABF4B3 /* zh-Hant */, - ); - name = InfoPlist.strings; - path = ..; - sourceTree = ""; - }; - 6A0D4F4A15FC0EE100ABF4B3 /* Localizable.strings */ = { - isa = PBXVariantGroup; - children = ( - 6A0D4F4B15FC0EE100ABF4B3 /* en */, - 6A0D4F5515FC0EF900ABF4B3 /* zh-Hant */, - ); - name = Localizable.strings; - path = ..; - sourceTree = ""; - }; 6A0D4F4E15FC0EE100ABF4B3 /* preferences.xib */ = { isa = PBXVariantGroup; children = ( @@ -890,6 +876,26 @@ path = Source/Installer; sourceTree = SOURCE_ROOT; }; + D4E33D8827A838CF006DB1CF /* Localizable.strings */ = { + isa = PBXVariantGroup; + children = ( + D4E33D8927A838CF006DB1CF /* Base */, + D4E33D8B27A838D5006DB1CF /* en */, + D4E33D8C27A838D8006DB1CF /* zh-Hant */, + ); + name = Localizable.strings; + sourceTree = ""; + }; + D4E33D8D27A838F0006DB1CF /* InfoPlist.strings */ = { + isa = PBXVariantGroup; + children = ( + D4E33D8E27A838F0006DB1CF /* Base */, + D4E33D9027A838F4006DB1CF /* en */, + D4E33D9127A838F7006DB1CF /* zh-Hant */, + ); + name = InfoPlist.strings; + sourceTree = ""; + }; D4F0BBE9279B14C20071253C /* Credits.rtf */ = { isa = PBXVariantGroup; children = ( diff --git a/Source/Base.lproj/InfoPlist.strings b/Source/Base.lproj/InfoPlist.strings new file mode 100644 index 00000000..9414dcd8 --- /dev/null +++ b/Source/Base.lproj/InfoPlist.strings @@ -0,0 +1,5 @@ +CFBundleName = "McBopomofo"; +CFBundleDisplayName = "McBopomofo"; +NSHumanReadableCopyright = "Copyright © 2011-2022 Mengjuei Hsieh et al.\nAll Rights Reserved."; +"org.openvanilla.inputmethod.McBopomofo.Bopomofo" = "Bopomofo"; +"org.openvanilla.inputmethod.McBopomofo.PlainBopomofo" = "Plain Bopomofo"; diff --git a/Source/Base.lproj/Localizable.strings b/Source/Base.lproj/Localizable.strings new file mode 100644 index 00000000..3f906806 --- /dev/null +++ b/Source/Base.lproj/Localizable.strings @@ -0,0 +1,92 @@ +/* No comment provided by engineer. */ +"About McBopomofo…" = "About McBopomofo…"; + +/* No comment provided by engineer. */ +"McBopomofo Preferences" = "McBopomofo Preferences"; + +/* No comment provided by engineer. */ +"Check for Updates…" = "Check for Updates…"; + +/* No comment provided by engineer. */ +"Update Check Failed" = "Update Check Failed"; + +/* No comment provided by engineer. */ +"There may be no internet connection or the server failed to respond.\n\nError message: %@" = "There may be no internet connection or the server failed to respond.\n\nError message: %@"; + +/* No comment provided by engineer. */ +"OK" = "OK"; + +/* No comment provided by engineer. */ +"Dismiss" = "Dismiss"; + +/* No comment provided by engineer. */ +"New Version Available" = "New Version Available"; + +/* No comment provided by engineer. */ +"Not Now" = "Not Now"; + +/* No comment provided by engineer. */ +"Visit Website" = "Visit Website"; + +/* No comment provided by engineer. */ +"You're currently using McBopomofo %@ (%@), a new version %@ (%@) is now available. Do you want to visit McBopomofo's website to download the version?%@" = "You're currently using McBopomofo %@ (%@), a new version %@ (%@) is now available. Do you want to visit McBopomofo's website to download the version?%@"; + +"Chinese Conversion" = "Convert to Simplified Chinese"; + +"User Phrases" = "User Phrases"; + +"Edit User Phrases" = "Edit User Phrases"; + +"Reload User Phrases" = "Reload User Phrases"; + +"Unable to create the user phrase file." = "Unable to create the user phrase file."; + +"Please check the permission of at \"%@\"." = "Please check the permission of at \"%@\"."; + +"Edit Excluded Phrases" = "Edit Excluded Phrases"; + +"Use Half-Width Punctuations" = "Use Half-Width Punctuations"; + +"You are now selecting \"%@\". You can add a phrase with two or more characters." = "You are now selecting \"%@\". You can add a phrase with two or more characters."; + +"You are now selecting \"%@\". Press enter to add a new phrase." = "You are now selecting \"%@\". Press enter to add a new phrase."; + +"You are now selecting \"%@\". A phrase cannot be longer than %d characters." = "You are now selecting \"%@\". A phrase cannot be longer than %d characters."; + +"Chinese conversion on" = "Chinese conversion on"; + +"Chinese conversion off" = "Chinese conversion off"; + +"Edit Phrase Replacement Table" = "Edit Phrase Replacement Table"; + +"Use Phrase Replacement" = "Use Phrase Replacement"; + +"Candidates keys cannot be empty." = "Candidates keys cannot be empty."; + +"Candidate keys can only contain latin characters and numbers." = "Candidate keys can only contain latin characters and numbers."; + +"Candidate keys cannot contain space." = "Candidate keys cannot contain space."; + +"There should not be duplicated keys." = "There should not be duplicated keys."; + +"The length of your candidate keys can not be less than 4 characters." = "The length of your candidate keys can not be less than 4 characters."; + +"The length of your candidate keys can not be larger than 15 characters." = "The length of your candidate keys can not be larger than 15 characters."; + +"Phrase replacement mode is on. Not suggested to add phrase in the mode." = "Phrase replacement mode is on. Not suggested to add phrase in the mode."; + +"Model based Chinese conversion is on. Not suggested to add phrase in the mode." = "Model based Chinese conversion is on. Not suggested to add phrase in the mode."; + +"Half-width punctuation on" = "Half-width punctuation on"; + +"Half-width punctuation off" = "Half-width punctuation off"; + +"Associated Phrases" = "Associated Phrases"; + +"There are special phrases in your text. We don't support adding new phrases in this case." = "There are special phrases in your text. We don't support adding new phrases in this case."; + +"Cursor is before \"%@\"." = "Cursor is before \"%@\"."; + +"Cursor is after \"%@\"." = "Cursor is after \"%@\"."; + +"Cursor is between \"%@\" and \"%@\"." = "Cursor is between \"%@\" and \"%@\"."; diff --git a/Source/InputMethodController.swift b/Source/InputMethodController.swift index 3b0982f6..f236fb5a 100644 --- a/Source/InputMethodController.swift +++ b/Source/InputMethodController.swift @@ -180,6 +180,7 @@ class McBopomofoInputMethodController: IMKInputController { } let input = KeyHandlerInput(event: event, isVerticalMode: useVerticalMode) + let result = keyHandler.handle(input, state: state) { newState in self.handle(state: newState, client: client) } candidateSelectionCallback: { @@ -388,6 +389,9 @@ extension McBopomofoInputMethodController { // the selection range is where the cursor is, with the length being 0 and replacement range NSNotFound, // i.e. the client app needs to take care of where to put this composing buffer client.setMarkedText(state.attributedString, selectionRange: NSMakeRange(Int(state.cursorIndex), 0), replacementRange: NSMakeRange(NSNotFound, NSNotFound)) + if !state.tooltip.isEmpty { + show(tooltip: state.tooltip, composingBuffer: state.composingBuffer, cursorIndex: state.cursorIndex, client: client) + } } private func handle(state: InputState.Marking, previous: InputState, client: Any?) { @@ -482,16 +486,19 @@ extension McBopomofoInputMethodController { gCurrentCandidateController?.visible = true var lineHeightRect = NSMakeRect(0.0, 0.0, 16.0, 16.0) - var cursor: UInt = 0 + var cursor: Int = 0 if let state = state as? InputState.ChoosingCandidate { - cursor = state.cursorIndex + cursor = Int(state.cursorIndex) if cursor == state.composingBuffer.count && cursor != 0 { cursor -= 1 } } - (client as? IMKTextInput)?.attributes(forCharacterIndex: Int(cursor), lineHeightRectangle: &lineHeightRect) + while lineHeightRect.origin.x == 0 && lineHeightRect.origin.y == 0 && cursor >= 0 { + (client as? IMKTextInput)?.attributes(forCharacterIndex: cursor, lineHeightRectangle: &lineHeightRect) + cursor -= 1 + } if useVerticalMode { gCurrentCandidateController?.set(windowTopLeftPoint: NSMakePoint(lineHeightRect.origin.x + lineHeightRect.size.width + 4.0, lineHeightRect.origin.y - 4.0), bottomOutOfScreenAdjustmentHeight: lineHeightRect.size.height + 4.0) @@ -502,11 +509,14 @@ extension McBopomofoInputMethodController { private func show(tooltip: String, composingBuffer: String, cursorIndex: UInt, client: Any!) { var lineHeightRect = NSMakeRect(0.0, 0.0, 16.0, 16.0) - var cursor = cursorIndex + var cursor: Int = Int(cursorIndex) if cursor == composingBuffer.count && cursor != 0 { cursor -= 1 } - (client as? IMKTextInput)?.attributes(forCharacterIndex: Int(cursor), lineHeightRectangle: &lineHeightRect) + while lineHeightRect.origin.x == 0 && lineHeightRect.origin.y == 0 && cursor >= 0 { + (client as? IMKTextInput)?.attributes(forCharacterIndex: cursor, lineHeightRectangle: &lineHeightRect) + cursor -= 1 + } McBopomofoInputMethodController.tooltipController.show(tooltip: tooltip, at: lineHeightRect.origin) } diff --git a/Source/InputState.swift b/Source/InputState.swift index 6483c626..8dd6780e 100644 --- a/Source/InputState.swift +++ b/Source/InputState.swift @@ -64,6 +64,8 @@ class InputState: NSObject { } } + // MARK: - + /// Represents that the composing buffer is empty. @objc (InputStateEmpty) class Empty: InputState { @@ -76,6 +78,8 @@ class InputState: NSObject { } } + // MARK: - + /// Represents that the composing buffer is empty. @objc (InputStateEmptyIgnoringPreviousState) class EmptyIgnoringPreviousState: InputState { @@ -87,6 +91,8 @@ class InputState: NSObject { } } + // MARK: - + /// Represents that the input controller is committing text into client app. @objc (InputStateCommitting) class Committing: InputState { @@ -101,11 +107,14 @@ class InputState: NSObject { "" } } + + // MARK: - + /// Represents that the composing buffer is not empty. @objc (InputStateNotEmpty) class NotEmpty: InputState { - @objc private(set) var composingBuffer: String = "" - @objc private(set) var cursorIndex: UInt = 0 + @objc private(set) var composingBuffer: String + @objc private(set) var cursorIndex: UInt @objc init(composingBuffer: String, cursorIndex: UInt) { self.composingBuffer = composingBuffer @@ -117,12 +126,13 @@ class InputState: NSObject { } } + // MARK: - + /// Represents that the user is inputting text. @objc (InputStateInputting) class Inputting: NotEmpty { - @objc var bpmfReading: String = "" - @objc var bpmfReadingCursorIndex: UInt8 = 0 @objc var poppedText: String = "" + @objc var tooltip: String = "" @objc override init(composingBuffer: String, cursorIndex: UInt) { super.init(composingBuffer: composingBuffer, cursorIndex: cursorIndex) @@ -137,20 +147,27 @@ class InputState: NSObject { } override var description: String { - "" + ", poppedText:\(poppedText)>" } } + // MARK: - + private let kMinMarkRangeLength = 2 private let kMaxMarkRangeLength = 6 /// Represents that the user is marking a range in the composing buffer. @objc (InputStateMarking) class Marking: NotEmpty { + @objc private(set) var markerIndex: UInt @objc private(set) var markedRange: NSRange @objc var tooltip: String { + if composingBuffer.count != readings.count { + return NSLocalizedString("There are special phrases in your text. We don't support adding new phrases in this case.", comment: "") + } + if Preferences.phraseReplacementEnabled { return NSLocalizedString("Phrase replacement mode is on. Not suggested to add phrase in the mode.", comment: "") } @@ -170,6 +187,7 @@ class InputState: NSObject { return String(format: NSLocalizedString("You are now selecting \"%@\". Press enter to add a new phrase.", comment: ""), text) } + @objc var tooltipForInputting: String = "" @objc private(set) var readings: [String] @objc init(composingBuffer: String, cursorIndex: UInt, markerIndex: UInt, readings: [String]) { @@ -197,32 +215,45 @@ class InputState: NSObject { .underlineStyle: NSUnderlineStyle.single.rawValue, .markedClauseSegment: 2 ], range: NSRange(location: end, - length: composingBuffer.count - end)) + length: (composingBuffer as NSString).length - end)) return attributedSting } override var description: String { - "" + "" } @objc func convertToInputting() -> Inputting { let state = Inputting(composingBuffer: composingBuffer, cursorIndex: cursorIndex) + state.tooltip = tooltipForInputting return state } @objc var validToWrite: Bool { - markedRange.length >= kMinMarkRangeLength && markedRange.length <= kMaxMarkRangeLength + /// McBopomofo allows users to input a string whose length differs + /// from the amount of Bopomofo readings. In this case, the range + /// in the composing buffer and the readings could not match, so + /// we disable the function to write user phrases in this case. + if composingBuffer.count != readings.count { + return false + } + + return markedRange.length >= kMinMarkRangeLength && + markedRange.length <= kMaxMarkRangeLength } @objc var userPhrase: String { let text = (composingBuffer as NSString).substring(with: markedRange) - let end = markedRange.location + markedRange.length - let readings = readings[markedRange.location.." + "" } } + // MARK: - + /// Represents that the user is choosing in a candidates list /// in the associated phrases mode. @objc (InputStateAssociatedPhrases) @@ -264,5 +297,4 @@ class InputState: NSObject { "" } } - } diff --git a/Source/KeyHandler.mm b/Source/KeyHandler.mm index 44d53e05..569eca31 100644 --- a/Source/KeyHandler.mm +++ b/Source/KeyHandler.mm @@ -226,12 +226,12 @@ static NSString *const kGraphVizOutputfile = @"/tmp/McBopomofo-visualization.dot McBopomofoEmacsKey emacsKey = input.emacsKey; // if the inputText is empty, it's a function key combination, we ignore it - if (![input.inputText length]) { + if (!input.inputText.length) { return NO; } // if the composing buffer is empty and there's no reading, and there is some function key combination, we ignore it - BOOL isFunctionKey = ([input isCommandHold] || [input isControlHold] || [input isOptionHold] || [input isNumericPad]); + BOOL isFunctionKey = ([input isCommandHold] || [input isOptionHold] || [input isNumericPad]) || [input isControlHotKey]; if (![state isKindOfClass:[InputStateNotEmpty class]] && ![state isKindOfClass:[InputStateAssociatedPhrases class]] && isFunctionKey) { @@ -302,10 +302,12 @@ static NSString *const kGraphVizOutputfile = @"/tmp/McBopomofo-visualization.dot } bool composeReading = false; + BOOL skipBpmfHandling = [input isReservedKey] || [input isControlHold]; // MARK: Handle BPMF Keys + // see if it's valid BPMF reading - if (_bpmfReadingBuffer->isValidKey((char) charCode)) { + if (!skipBpmfHandling && _bpmfReadingBuffer->isValidKey((char) charCode)) { _bpmfReadingBuffer->combineKey((char) charCode); // if we have a tone marker, we have to insert the reading to the @@ -348,7 +350,7 @@ static NSString *const kGraphVizOutputfile = @"/tmp/McBopomofo-visualization.dot size_t cursorIndex = [self _actualCandidateCursorIndex]; vector nodes = _builder->grid().nodesCrossingOrEndingAt(cursorIndex); double highestScore = FindHighestScore(nodes, kEpsilon); - _builder->grid().overrideNodeScoreForSelectedCandidate(cursorIndex, overrideValue, highestScore); + _builder->grid().overrideNodeScoreForSelectedCandidate(cursorIndex, overrideValue, static_cast(highestScore)); } // then update the text @@ -397,12 +399,10 @@ static NSString *const kGraphVizOutputfile = @"/tmp/McBopomofo-visualization.dot // if the spacebar is NOT set to be a selection key if ([input isShiftHold] || !Preferences.chooseCandidateUsingSpace) { if (_builder->cursorIndex() >= _builder->length()) { - if ([state isKindOfClass:[InputStateNotEmpty class]]) { - NSString *composingBuffer = [(InputStateNotEmpty *)state composingBuffer]; - if ([composingBuffer length]) { - InputStateCommitting *committing = [[InputStateCommitting alloc] initWithPoppedText:composingBuffer]; - stateCallback(committing); - } + NSString *composingBuffer = [(InputStateNotEmpty*) state composingBuffer]; + if (composingBuffer.length) { + InputStateCommitting *committing = [[InputStateCommitting alloc] initWithPoppedText:composingBuffer]; + stateCallback (committing); } [self clear]; InputStateCommitting *committing = [[InputStateCommitting alloc] initWithPoppedText:@" "]; @@ -490,8 +490,16 @@ static NSString *const kGraphVizOutputfile = @"/tmp/McBopomofo-visualization.dot // MARK: Punctuation // if nothing is matched, see if it's a punctuation key for current layout. + + string punctuationNamePrefix; + if ([input isControlHold]) { + punctuationNamePrefix = string("_ctrl_punctuation_"); + } else if (Preferences.halfWidthPunctuationEnabled) { + punctuationNamePrefix = string("_half_punctuation_"); + } else { + punctuationNamePrefix = string("_punctuation_"); + } string layout = [self _currentLayout]; - string punctuationNamePrefix = Preferences.halfWidthPunctuationEnabled ? string("_half_punctuation_") : string("_punctuation_"); string customPunctuation = punctuationNamePrefix + layout + string(1, (char) charCode); if ([self _handlePunctuation:customPunctuation state:state usingVerticalMode:input.useVerticalMode stateCallback:stateCallback errorCallback:errorCallback]) { return YES; @@ -540,7 +548,7 @@ static NSString *const kGraphVizOutputfile = @"/tmp/McBopomofo-visualization.dot // other platforms if (_bpmfReadingBuffer->isEmpty()) { - // no nee to beep since the event is deliberately triggered by user + // no need to beep since the event is deliberately triggered by user if (![state isKindOfClass:[InputStateInputting class]]) { return NO; } @@ -569,8 +577,10 @@ static NSString *const kGraphVizOutputfile = @"/tmp/McBopomofo-visualization.dot if ([input isShiftHold]) { // Shift + left - if (_builder->cursorIndex() > 0) { - InputStateMarking *marking = [[InputStateMarking alloc] initWithComposingBuffer:currentState.composingBuffer cursorIndex:currentState.cursorIndex markerIndex:currentState.cursorIndex - 1 readings: [self _currentReadings]]; + if (currentState.cursorIndex > 0) { + NSInteger previousPosition = [StringUtils previousUtf16PositionForIndex:currentState.cursorIndex in:currentState.composingBuffer]; + InputStateMarking *marking = [[InputStateMarking alloc] initWithComposingBuffer:currentState.composingBuffer cursorIndex:currentState.cursorIndex markerIndex:previousPosition readings:[self _currentReadings]]; + marking.tooltipForInputting = currentState.tooltip; stateCallback(marking); } else { errorCallback(); @@ -605,8 +615,10 @@ static NSString *const kGraphVizOutputfile = @"/tmp/McBopomofo-visualization.dot if ([input isShiftHold]) { // Shift + Right - if (_builder->cursorIndex() < _builder->length()) { - InputStateMarking *marking = [[InputStateMarking alloc] initWithComposingBuffer:currentState.composingBuffer cursorIndex:currentState.cursorIndex markerIndex:currentState.cursorIndex + 1 readings: [self _currentReadings]]; + if (currentState.cursorIndex < currentState.composingBuffer.length) { + NSInteger nextPosition = [StringUtils nextUtf16PositionForIndex:currentState.cursorIndex in:currentState.composingBuffer]; + InputStateMarking *marking = [[InputStateMarking alloc] initWithComposingBuffer:currentState.composingBuffer cursorIndex:currentState.cursorIndex markerIndex:nextPosition readings:[self _currentReadings]]; + marking.tooltipForInputting = currentState.tooltip; stateCallback(marking); } else { errorCallback(); @@ -832,8 +844,9 @@ static NSString *const kGraphVizOutputfile = @"/tmp/McBopomofo-visualization.dot && ([input isShiftHold])) { NSUInteger index = state.markerIndex; if (index > 0) { - index -= 1; + index = [StringUtils previousUtf16PositionForIndex:index in:state.composingBuffer]; InputStateMarking *marking = [[InputStateMarking alloc] initWithComposingBuffer:state.composingBuffer cursorIndex:state.cursorIndex markerIndex:index readings:state.readings]; + marking.tooltipForInputting = state.tooltipForInputting; stateCallback(marking); } else { errorCallback(); @@ -847,8 +860,9 @@ static NSString *const kGraphVizOutputfile = @"/tmp/McBopomofo-visualization.dot && ([input isShiftHold])) { NSUInteger index = state.markerIndex; if (index < state.composingBuffer.length) { - index += 1; + index = [StringUtils nextUtf16PositionForIndex:index in:state.composingBuffer]; InputStateMarking *marking = [[InputStateMarking alloc] initWithComposingBuffer:state.composingBuffer cursorIndex:state.cursorIndex markerIndex:index readings:state.readings]; + marking.tooltipForInputting = state.tooltipForInputting; stateCallback(marking); } else { errorCallback(); @@ -1070,7 +1084,14 @@ static NSString *const kGraphVizOutputfile = @"/tmp/McBopomofo-visualization.dot if (_inputMode == InputModePlainBopomofo) { string layout = [self _currentLayout]; - string punctuationNamePrefix = Preferences.halfWidthPunctuationEnabled ? string("_half_punctuation_") : string("_punctuation_"); + string punctuationNamePrefix; + if ([input isControlHold]) { + punctuationNamePrefix = string("_ctrl_punctuation_"); + } else if (Preferences.halfWidthPunctuationEnabled) { + punctuationNamePrefix = string("_half_punctuation_"); + } else { + punctuationNamePrefix = string("_punctuation_"); + } string customPunctuation = punctuationNamePrefix + layout + string(1, (char) charCode); string punctuation = punctuationNamePrefix + string(1, (char) charCode); @@ -1114,6 +1135,8 @@ static NSString *const kGraphVizOutputfile = @"/tmp/McBopomofo-visualization.dot size_t readingCursorIndex = 0; size_t builderCursorIndex = _builder->cursorIndex(); + NSString *tooltip = @""; + // we must do some Unicode codepoint counting to find the actual cursor location for the client // i.e. we need to take UTF-16 into consideration, for which a surrogate pair takes 2 UniChars // locations @@ -1137,9 +1160,30 @@ static NSString *const kGraphVizOutputfile = @"/tmp/McBopomofo-visualization.dot composedStringCursorIndex += [valueString length]; readingCursorIndex += spanningLength; } else { - for (size_t i = 0; i < codepointCount && readingCursorIndex < builderCursorIndex; i++) { - composedStringCursorIndex += [[NSString stringWithUTF8String:codepoints[i].c_str()] length]; - readingCursorIndex++; + if (codepointCount == spanningLength) { + for (size_t i = 0; i < codepointCount && readingCursorIndex < builderCursorIndex; i++) { + composedStringCursorIndex += [[NSString stringWithUTF8String:codepoints[i].c_str()] length]; + readingCursorIndex++; + } + } else { + if (readingCursorIndex < builderCursorIndex) { + composedStringCursorIndex += [valueString length]; + readingCursorIndex += spanningLength; + if (readingCursorIndex > builderCursorIndex) { + readingCursorIndex = builderCursorIndex; + } + if (builderCursorIndex == 0) { + tooltip = [NSString stringWithFormat:NSLocalizedString(@"Cursor is before \"%@\".", @""), + [NSString stringWithUTF8String:_builder->readings()[builderCursorIndex].c_str()]]; + } else if (builderCursorIndex >= _builder->readings().size()) { + tooltip = [NSString stringWithFormat:NSLocalizedString(@"Cursor is after \"%@\".", @""), + [NSString stringWithUTF8String:_builder->readings()[_builder->readings().size() - 1].c_str()]]; + } else { + tooltip = [NSString stringWithFormat:NSLocalizedString(@"Cursor is between \"%@\" and \"%@\".", @""), + [NSString stringWithUTF8String:_builder->readings()[builderCursorIndex - 1].c_str()], + [NSString stringWithUTF8String:_builder->readings()[builderCursorIndex].c_str()]]; + } + } } } } @@ -1155,6 +1199,7 @@ static NSString *const kGraphVizOutputfile = @"/tmp/McBopomofo-visualization.dot NSInteger cursorIndex = composedStringCursorIndex + [reading length]; InputStateInputting *newState = [[InputStateInputting alloc] initWithComposingBuffer:composedText cursorIndex:cursorIndex]; + newState.tooltip = tooltip; return newState; } diff --git a/Source/KeyHandlerInput.swift b/Source/KeyHandlerInput.swift index ad86c1b2..2e4fbfa7 100644 --- a/Source/KeyHandlerInput.swift +++ b/Source/KeyHandlerInput.swift @@ -90,6 +90,10 @@ class KeyHandlerInput: NSObject { super.init() } + override var description: String { + return "<\(super.description) inputText:\(String(describing: inputText)), inputTextIgnoringModifiers:\(String(describing: inputTextIgnoringModifiers)) charCode:\(charCode), keyCode:\(keyCode), flags:\(flags), cursorForwardKey:\(cursorForwardKey), cursorBackwardKey:\(cursorBackwardKey), extraChooseCandidateKey:\(extraChooseCandidateKey), absorbedArrowKey:\(absorbedArrowKey), verticalModeOnlyChooseCandidateKey:\(verticalModeOnlyChooseCandidateKey), emacsKey:\(emacsKey), useVerticalMode:\(useVerticalMode)>" + } + @objc var isShiftHold: Bool { flags.contains([.shift]) } @@ -102,6 +106,10 @@ class KeyHandlerInput: NSObject { flags.contains([.control]) } + @objc var isControlHotKey: Bool { + flags.contains([.control]) && inputText?.first?.isLetter ?? false + } + @objc var isOptionHold: Bool { flags.contains([.option]) } @@ -114,6 +122,13 @@ class KeyHandlerInput: NSObject { flags.contains([.numericPad]) } + @objc var isReservedKey: Bool { + guard let code = KeyCode(rawValue: keyCode) else { + return false + } + return code.rawValue != KeyCode.none.rawValue + } + @objc var isEnter: Bool { KeyCode(rawValue: keyCode) == KeyCode.enter } diff --git a/Source/Preferences.swift b/Source/Preferences.swift index 287f7dba..dd332818 100644 --- a/Source/Preferences.swift +++ b/Source/Preferences.swift @@ -219,7 +219,6 @@ class Preferences: NSObject { defaults.removeObject(forKey: kChineseConversionEngineKey) defaults.removeObject(forKey: kChineseConversionStyle) defaults.removeObject(forKey: kAssociatedPhrasesEnabledKey) -// defaults.removeObject(forKey: kAssociatedPhrasesKeys) } @UserDefault(key: kKeyboardLayoutPreferenceKey, defaultValue: 0) diff --git a/Source/StringUtils.swift b/Source/StringUtils.swift new file mode 100644 index 00000000..db5067a8 --- /dev/null +++ b/Source/StringUtils.swift @@ -0,0 +1,73 @@ +// Copyright (c) 2022 and onwards The McBopomofo Authors. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the "Software"), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. + +import Foundation + +/// Utilities to convert the length of an NSString and a Swift string. +class StringUtils: NSObject { + + /// Converts the index in an NSString to the index in a Swift string. + /// + /// An Emoji might be compose by more than one UTF-16 code points, however + /// the length of an NSString is only the sum of the UTF-16 code points. It + /// causes that the NSString and Swift string representation of the same + /// string have different lengths once the string contains such Emoji. The + /// method helps to find the index in a Swift string by passing the index + /// in an NSString. + static func convertToCharIndex(from utf16Index: Int, in string: String) -> Int { + var length = 0 + for (i, character) in string.enumerated() { + if length >= utf16Index { + return i + } + length += character.utf16.count + } + return string.count + } + + @objc (nextUtf16PositionForIndex:in:) + static func nextUtf16Position(for index: Int, in string: String) -> Int { + var index = convertToCharIndex(from: index, in: string) + if index < string.count { + index += 1 + } + let count = string[.. Int { + var index = convertToCharIndex(from: index, in: string) + if index > 0 { + index -= 1 + } + let count = string[..