Open-source keyboard layout text converter for macOS.
Typed text in the wrong keyboard layout? Select it, press a hotkey, and LangSwitcher instantly converts it to the correct layout. No more retyping ghbdtn when you meant привет.
An open-source alternative to Caramba Switcher and Punto Switcher.
If you find LangSwitcher useful, consider supporting the project — any contribution is appreciated!
Documentation | Документация (RU)
https://github.com/reg2005/langSwitcher/raw/main/screenshots/langSwitch.mp4
- Instant text conversion — select text and press a hotkey to convert between layouts
- Smart Conversion modes — works even without manual text selection:
- Greedy Line (default) — selects to line start, finds where wrong layout begins, converts the entire wrong-layout phrase
- Last Word — auto-selects and converts only the last typed word
- Disabled — only works with explicit text selection
- Auto-detection — automatically detects which layout the text was typed in
- System keyboard integration — uses your installed system keyboard layouts
- Double Shift hotkey — press
⇧⇧(Shift twice quickly) to convert, or set a custom shortcut - Conversion Log — optionally log conversions to a local SQLite database (disabled by default for privacy). Review and label entries (correct/incorrect) for future ML training
- JSON export — export conversion logs for data analysis or model training
- Menu bar app — lives quietly in your status bar, always ready
- Multiple layouts — English, Russian, German, French, Spanish (5 layouts)
- Punctuation preservation —
?,!,/and other punctuation stay unchanged during conversion - Zero dependencies — pure Swift, no external libraries, no dictionaries
- Privacy first — no data leaves your Mac, no analytics, no network access
- Open source — MIT licensed, contributions welcome
1. You type "ghbdtn" (meant to type "привет" but had English layout active)
2. Select the mistyped text (or just press the hotkey — Smart Conversion handles it)
3. Press ⇧⇧ (double Shift)
4. Text is replaced with "привет"
LangSwitcher maps characters based on physical key positions on the keyboard. The same physical key produces different characters depending on the active layout — LangSwitcher reverses this mapping.
Go to Releases and download:
| Architecture | File |
|---|---|
| Apple Silicon (M1/M2/M3/M4) | LangSwitcher-*-arm64.dmg |
| Intel | LangSwitcher-*-x86_64.dmg |
| Universal (both) | LangSwitcher-*-universal.dmg |
- Open the DMG and drag LangSwitcher to Applications
- Launch LangSwitcher
- Grant Accessibility permission when prompted
- macOS 13.0 (Ventura) or later
- Xcode 15.0 or later
# Clone the repository
git clone https://github.com/reg2005/langSwitcher.git
cd langSwitcher
# Run tests
xcodebuild test \
-project LangSwitcher.xcodeproj \
-scheme LangSwitcher \
-destination 'platform=macOS' \
CODE_SIGN_IDENTITY="-" \
CODE_SIGNING_REQUIRED=NO \
CODE_SIGNING_ALLOWED=NO
# Build (universal binary)
xcodebuild -project LangSwitcher.xcodeproj \
-scheme LangSwitcher \
-configuration Release \
-derivedDataPath build \
-arch arm64 -arch x86_64 \
ONLY_ACTIVE_ARCH=NO \
CODE_SIGN_IDENTITY="-" \
CODE_SIGNING_REQUIRED=NO \
CODE_SIGNING_ALLOWED=NO \
build
# The app is at build/Build/Products/Release/LangSwitcher.appOr open LangSwitcher.xcodeproj in Xcode and press ⌘R.
- Type text in any application
- Realize you had the wrong keyboard layout active
- Press
⇧⇧(double Shift) — Smart Conversion auto-selects and converts - Or select the mistyped text manually, then press the hotkey
- The text is instantly converted to the correct layout
| Mode | Behavior |
|---|---|
| Greedy Line (default) | Selects to line start, finds where wrong layout begins, converts the entire wrong-layout phrase. Handles "ghbdtn rfr ltkf lheu" -> "привет как дела друг" |
| Last Word | Auto-selects only the last word before the cursor, converts if it looks like wrong layout |
| Disabled | Only works with explicit manual text selection |
LangSwitcher lives in your menu bar with a keyboard icon. Click it to:
- Convert selected text manually
- See active keyboard layouts
- View conversion statistics
- Open settings
- Quit the app
Access settings from the menu bar icon -> Settings (or ⌘,):
| Tab | Description |
|---|---|
| General | Launch at login, sounds, notifications, Smart Conversion mode, Layout Switch mode |
| Layouts | View and refresh detected keyboard layouts |
| Hotkey | Toggle double-shift or record a custom shortcut |
| Permissions | Check and grant Accessibility access |
| Log | View conversion history, label entries as correct/incorrect, export to JSON |
Conversion logging is disabled by default for your privacy. You can enable it in Settings -> General -> Conversion Logging.
When enabled, conversions are saved to a local SQLite database (~/Library/Application Support/LangSwitcher/conversion_log.sqlite). You can configure the maximum number of stored entries (default: 100, or 0 for unlimited). In the Log tab you can:
- Browse all past conversions (input -> output, layouts, mode, timestamp)
- Rate each conversion as correct or incorrect (tri-state: unrated / correct / incorrect)
- Export the labeled data as JSON for ML training or analysis
- Delete individual entries or clear the entire log
No data ever leaves your Mac. The conversion log is stored purely locally and is never transmitted anywhere.
LangSwitcher detects layouts from your System Settings > Keyboard > Input Sources. Currently supported:
| Layout | Language Code | Physical Layout |
|---|---|---|
| U.S. (QWERTY) | en |
QWERTY |
| ABC | en |
QWERTY |
| Russian | ru |
ЙЦУКЕН |
| German | de |
QWERTZ |
| French | fr |
AZERTY |
| Spanish | es |
QWERTY (Spanish) |
Adding a new layout is straightforward — see Contributing.
LangSwitcher/
├── Sources/
│ ├── App/
│ │ ├── LangSwitcherApp.swift # SwiftUI App entry point
│ │ ├── AppDelegate.swift # App lifecycle, hotkey, conversion orchestration
│ │ └── StatusBarController.swift # Menu bar icon and menu
│ ├── Views/
│ │ ├── SettingsView.swift # Settings window (5 tabs)
│ │ ├── ConversionLogView.swift # Conversion log with data labeling
│ │ ├── HotkeyRecorderView.swift # Custom hotkey recorder
│ │ ├── AboutView.swift # About window
│ │ └── PermissionsView.swift # Accessibility permissions
│ ├── Services/
│ │ ├── LayoutMapper.swift # Character mapping engine
│ │ ├── KeyboardLayoutDetector.swift # System layout detection (Carbon TIS)
│ │ ├── TextConverter.swift # Conversion orchestrator + greedy algorithm
│ │ ├── ConversionLogStore.swift # SQLite-based conversion log storage
│ │ ├── HotkeyManager.swift # Global hotkey (double-shift + custom)
│ │ ├── AccessibilityService.swift # Clipboard-based text replacement
│ │ └── SettingsManager.swift # UserDefaults persistence
│ ├── Localization/
│ │ ├── LocalizationManager.swift # Runtime i18n engine
│ │ ├── Strings_en.swift # English strings (~113 keys)
│ │ └── Strings_ru.swift # Russian strings
│ └── Models/
│ ├── KeyboardLayout.swift # Layout model & character maps
│ └── ConversionLog.swift # Conversion log entry model
├── Resources/
│ └── Assets.xcassets # App icons and colors
├── LangSwitcherTests/
│ ├── LayoutMapperTests.swift # 58 mapping tests
│ └── TextConverterTests.swift # 35 converter tests
├── .github/workflows/
│ ├── build.yml # CI: test + build DMGs (Intel/ARM/Universal) + release
│ └── pages.yml # GitHub Pages deployment
├── screenshots/
│ ├── general.png # Settings General tab
│ └── langSwitch.mp4 # Demo video
└── docs/ # Docus v4 documentation site
├── nuxt.config.ts # Nuxt/Docus config with i18n
├── app.config.ts # App config (header, SEO, socials)
├── package.json # Docus dependencies
└── content/
├── en/ # English documentation
└── ru/ # Russian documentation
Input: "ghbdtn" (typed on US layout when Russian was intended)
1. Detect source layout -> "US" (characters match US keyboard map)
2. For each character, find physical key position:
g -> key at position [0x05]
h -> key at position [0x04]
...
3. Map physical key to target layout (Russian):
[0x05] -> п
[0x04] -> р
...
4. Apply punctuation preservation (non-letter -> non-letter stays as-is)
5. Result: "привет"
- Clipboard-based replacement: Uses
⌘C-> transform ->⌘Vapproach for maximum app compatibility. Works in virtually any text field. - No CGEvent tap: Avoids
CGEventTapwhich requires special entitlements. Instead usesNSEvent.addGlobalMonitorForEventsfor hotkey detection andCGEventwith.hidSystemStatefor keyboard simulation. - Physical key mapping: Maps characters through their physical key position, not Unicode translation tables. This is more reliable for non-standard layouts.
- No sandbox: The app requires Accessibility access which is incompatible with App Sandbox.
- SQLite for logging: Direct SQLite3 C API — no external dependencies. Data stays local for privacy.
- Greedy two-pass algorithm: Pass 1 checks if the whole line is wrong-layout (all words switch script). Pass 2 scans right-to-left for mixed lines. The 70% threshold handles ambiguous cases.
| Component | Technology |
|---|---|
| Language | Swift 5 |
| UI Framework | SwiftUI |
| Platform | macOS 13+ (Ventura) |
| Input Detection | Carbon (TISInputSource) |
| Text Manipulation | Accessibility API + Pasteboard |
| Hotkey | NSEvent global monitor + CGEvent |
| Settings | UserDefaults |
| Conversion Log | SQLite3 (C API, no dependencies) |
| Localization | Custom runtime i18n (English, Russian) |
| CI/CD | GitHub Actions |
| Distribution | DMG (Intel + ARM + Universal) via GitHub Releases |
| Website | GitHub Pages |
| Tests | XCTest (93 tests) |
LangSwitcher requires Accessibility access to:
- Read selected text (via simulated
⌘C) - Replace text (via simulated
⌘V)
Grant access in System Settings -> Privacy & Security -> Accessibility.
The app does not:
- Log keystrokes
- Send data to any server — all data stays on your Mac
- Access files or network
- Run in the background when quit
- Collect any analytics or telemetry
Conversion logging is disabled by default. When you enable it, all data is stored locally in ~/Library/Application Support/LangSwitcher/. Nothing is ever uploaded or shared.
Contributions are welcome! Here's how to add a new keyboard layout:
- Open
LangSwitcher/Sources/Models/KeyboardLayout.swift - Add a new static property to
LayoutCharacterMapwith the character mapping - Add the pattern to
allMapsarray (order matters — specific patterns before generic ones) - Add tests in
LangSwitcherTests/LayoutMapperTests.swift - Run tests: all 93+ must pass
// Example: adding Italian layout
static let italian: [Character: Character] = {
let qwerty = Array("`1234567890-=qwertyuiop[]\\asdfghjkl;'zxcvbnm,./...")
let italian = Array("\\1234567890'ìqwertyuiopè+ùasdfghjklòàzxcvbnm,.-...")
var map: [Character: Character] = [:]
for i in 0..<min(qwerty.count, italian.count) {
map[qwerty[i]] = italian[i]
}
return map
}()LangSwitcher uses a custom localization system — no .lproj / .strings files. Language can be switched at runtime without restarting the app.
-
Copy the English template:
cp LangSwitcher/Sources/Localization/Strings_en.swift \ LangSwitcher/Sources/Localization/Strings_xx.swift
Replace
xxwith the language code (e.g.de,fr,es). -
Translate every value in the
stringsdictionary. Keys stay the same — only change the values. -
Update
register()at the bottom of your new file:@MainActor static func register() { LocalizationManager.shared.register(language: "xx", strings: strings) }
-
Register the new file in
LangSwitcherApp.swift:private func initializeLocalization() { Strings_en.register() Strings_ru.register() Strings_xx.register() // <-- add this }
-
Add to available languages in
LocalizationManager.swift:let availableLanguages: [(code: String, name: String)] = [ ("en", "English"), ("ru", "Русский"), ("xx", "Your Language"), // <-- add this ]
-
Add the file to Xcode project — add a
PBXBuildFile,PBXFileReference, add to the Localization group, and add toPBXSourcesBuildPhaseinproject.pbxproj. Follow the existingE1000001...PBX ID pattern. -
Run tests — all must pass.
String keys use namespace prefixes: menu.*, settings.*, general.*, smartMode.*, layouts.*, hotkey.*, permissions.*, log.*, about.*, alert.*, common.*.
Release builds can be signed with an Apple Developer ID certificate and notarized via GitHub Actions. Without secrets configured, builds use ad-hoc signing (works for local use, but macOS Gatekeeper will warn users).
Required secrets (Settings > Secrets and variables > Actions):
| Secret | Description |
|---|---|
MACOS_CERTIFICATE_P12 |
Developer ID Application certificate exported as .p12, then base64-encoded: base64 -i cert.p12 | pbcopy |
MACOS_CERTIFICATE_PASSWORD |
Password used when exporting the .p12 file |
MACOS_KEYCHAIN_PASSWORD |
Any random string (used for the temporary CI keychain) |
MACOS_SIGNING_IDENTITY |
Full identity string, e.g. Developer ID Application: Your Name (TEAMID) |
Optional secrets (for notarization — recommended for public distribution):
| Secret | Description |
|---|---|
MACOS_NOTARIZATION_APPLE_ID |
Your Apple ID email |
MACOS_NOTARIZATION_PASSWORD |
App-specific password (appleid.apple.com > Sign-In and Security > App-Specific Passwords) |
MACOS_NOTARIZATION_TEAM_ID |
Your 10-character Apple Developer Team ID |
How to get the certificate:
- Open Keychain Access on your Mac
- In the login keychain, find your "Developer ID Application" certificate
- Right-click > Export Items > save as
.p12with a password - Base64-encode it:
base64 -i DeveloperID.p12 | pbcopy - Paste the result into the
MACOS_CERTIFICATE_P12GitHub secret
Behavior:
- If
MACOS_CERTIFICATE_P12is set: builds are signed with your Developer ID + hardened runtime - If notarization secrets are also set: DMGs are submitted to Apple for notarization and stapled
- If no secrets are set: ad-hoc signing (forks and PRs work without any setup)
# Clone
git clone https://github.com/reg2005/langSwitcher.git
cd langSwitcher
# Run tests (required after every code change)
xcodebuild test \
-project LangSwitcher.xcodeproj \
-scheme LangSwitcher \
-destination 'platform=macOS' \
CODE_SIGN_IDENTITY="-" \
CODE_SIGNING_REQUIRED=NO \
CODE_SIGNING_ALLOWED=NO
# Open in Xcode
open LangSwitcher.xcodeprojSee AGENTS.md for AI agent / contributor guidelines.
LangSwitcher is free and open-source. If you find it useful, any contribution is welcome and helps keep the project alive. Thank you for your support!
| Network | Address |
|---|---|
| Ethereum | 0x30c8b011AF68a963694Ce1E5f54A545442acFEfA |
| Tron | TDAyNkS36eKyqv9s4KQpu4ebWciQ2bqdW3 |
| Bitcoin | bc1qtqm7rgma8dcgqc50lmzmyxn729yqrtf9zs7asx |
| Solana | A4jzTGxP7tbhyDFrcWJKdsA8v5xUwF6UzvC7RFmDRrfi |
| Linea | 0x30c8b011AF68a963694Ce1E5f54A545442acFEfA |
| Base | 0x30c8b011AF68a963694Ce1E5f54A545442acFEfA |
| BNB Chain | 0x30c8b011AF68a963694Ce1E5f54A545442acFEfA |
| Sei | 0x30c8b011AF68a963694Ce1E5f54A545442acFEfA |
| Polygon | 0x30c8b011AF68a963694Ce1E5f54A545442acFEfA |
| OP (Optimism) | 0x30c8b011AF68a963694Ce1E5f54A545442acFEfA |
| Arbitrum | 0x30c8b011AF68a963694Ce1E5f54A545442acFEfA |
| Fantom | 0x30c8b011AF68a963694Ce1E5f54A545442acFEfA |
MIT License — free for personal and commercial use.
Inspired by Caramba Switcher and Punto Switcher. Built as a free, open-source alternative.
