LangSwitcher

March 1, 2026 · View on GitHub

English | Русский

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.

Build License: MIT macOS 13+ Swift 5 Mentioned in Awesome

If you find LangSwitcher useful, consider supporting the project — any contribution is appreciated!

Documentation | Документация (RU)

Demo

https://github.com/reg2005/langSwitcher/raw/main/screenshots/langSwitch.mp4

Screenshot

LangSwitcher Settings — General

Key Features

  • 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

How It Works

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.

Download

Go to Releases and download:

ArchitectureFile
Apple Silicon (M1/M2/M3/M4)LangSwitcher-*-arm64.dmg
IntelLangSwitcher-*-x86_64.dmg
Universal (both)LangSwitcher-*-universal.dmg
  1. Open the DMG and drag LangSwitcher to Applications
  2. Launch LangSwitcher
  3. Grant Accessibility permission when prompted

Build from Source

Prerequisites

  • macOS 13.0 (Ventura) or later
  • Xcode 15.0 or later

Steps

# 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.app

Or open LangSwitcher.xcodeproj in Xcode and press ⌘R.

Usage

Basic Workflow

  1. Type text in any application
  2. Realize you had the wrong keyboard layout active
  3. Press ⇧⇧ (double Shift) — Smart Conversion auto-selects and converts
  4. Or select the mistyped text manually, then press the hotkey
  5. The text is instantly converted to the correct layout

Smart Conversion Modes

ModeBehavior
Greedy Line (default)Selects to line start, finds where wrong layout begins, converts the entire wrong-layout phrase. Handles "ghbdtn rfr ltkf lheu" -> "привет как дела друг"
Last WordAuto-selects only the last word before the cursor, converts if it looks like wrong layout
DisabledOnly 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

Settings

Access settings from the menu bar icon -> Settings (or ⌘,):

TabDescription
GeneralLaunch at login, sounds, notifications, Smart Conversion mode, Layout Switch mode
LayoutsView and refresh detected keyboard layouts
HotkeyToggle double-shift or record a custom shortcut
PermissionsCheck and grant Accessibility access
LogView conversion history, label entries as correct/incorrect, export to JSON

Conversion Log

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.

Supported Layouts

LangSwitcher detects layouts from your System Settings > Keyboard > Input Sources. Currently supported:

LayoutLanguage CodePhysical Layout
U.S. (QWERTY)enQWERTY
ABCenQWERTY
RussianruЙЦУКЕН
GermandeQWERTZ
FrenchfrAZERTY
SpanishesQWERTY (Spanish)

Adding a new layout is straightforward — see Contributing.

Architecture

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

How Conversion Works

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: "привет"

Key Design Decisions

  • Clipboard-based replacement: Uses ⌘C -> transform -> ⌘V approach for maximum app compatibility. Works in virtually any text field.
  • No CGEvent tap: Avoids CGEventTap which requires special entitlements. Instead uses NSEvent.addGlobalMonitorForEvents for hotkey detection and CGEvent with .hidSystemState for 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.

Tech Stack

ComponentTechnology
LanguageSwift 5
UI FrameworkSwiftUI
PlatformmacOS 13+ (Ventura)
Input DetectionCarbon (TISInputSource)
Text ManipulationAccessibility API + Pasteboard
HotkeyNSEvent global monitor + CGEvent
SettingsUserDefaults
Conversion LogSQLite3 (C API, no dependencies)
LocalizationCustom runtime i18n (English, Russian)
CI/CDGitHub Actions
DistributionDMG (Intel + ARM + Universal) via GitHub Releases
WebsiteGitHub Pages
TestsXCTest (93 tests)

Permissions

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.

Contributing

Contributions are welcome! Here's how to add a new keyboard layout:

  1. Open LangSwitcher/Sources/Models/KeyboardLayout.swift
  2. Add a new static property to LayoutCharacterMap with the character mapping
  3. Add the pattern to allMaps array (order matters — specific patterns before generic ones)
  4. Add tests in LangSwitcherTests/LayoutMapperTests.swift
  5. 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
}()

Adding a New Language (i18n)

LangSwitcher uses a custom localization system — no .lproj / .strings files. Language can be switched at runtime without restarting the app.

  1. Copy the English template:

    cp LangSwitcher/Sources/Localization/Strings_en.swift \
       LangSwitcher/Sources/Localization/Strings_xx.swift
    

    Replace xx with the language code (e.g. de, fr, es).

  2. Translate every value in the strings dictionary. Keys stay the same — only change the values.

  3. Update register() at the bottom of your new file:

    @MainActor static func register() {
        LocalizationManager.shared.register(language: "xx", strings: strings)
    }
    
  4. Register the new file in LangSwitcherApp.swift:

    private func initializeLocalization() {
        Strings_en.register()
        Strings_ru.register()
        Strings_xx.register()  // <-- add this
    }
    
  5. Add to available languages in LocalizationManager.swift:

    let availableLanguages: [(code: String, name: String)] = [
        ("en", "English"),
        ("ru", "Русский"),
        ("xx", "Your Language"),  // <-- add this
    ]
    
  6. Add the file to Xcode project — add a PBXBuildFile, PBXFileReference, add to the Localization group, and add to PBXSourcesBuildPhase in project.pbxproj. Follow the existing E1000001... PBX ID pattern.

  7. Run tests — all must pass.

String keys use namespace prefixes: menu.*, settings.*, general.*, smartMode.*, layouts.*, hotkey.*, permissions.*, log.*, about.*, alert.*, common.*.

Code Signing & Notarization (CI/CD)

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):

SecretDescription
MACOS_CERTIFICATE_P12Developer ID Application certificate exported as .p12, then base64-encoded: base64 -i cert.p12 | pbcopy
MACOS_CERTIFICATE_PASSWORDPassword used when exporting the .p12 file
MACOS_KEYCHAIN_PASSWORDAny random string (used for the temporary CI keychain)
MACOS_SIGNING_IDENTITYFull identity string, e.g. Developer ID Application: Your Name (TEAMID)

Optional secrets (for notarization — recommended for public distribution):

SecretDescription
MACOS_NOTARIZATION_APPLE_IDYour Apple ID email
MACOS_NOTARIZATION_PASSWORDApp-specific password (appleid.apple.com > Sign-In and Security > App-Specific Passwords)
MACOS_NOTARIZATION_TEAM_IDYour 10-character Apple Developer Team ID

How to get the certificate:

  1. Open Keychain Access on your Mac
  2. In the login keychain, find your "Developer ID Application" certificate
  3. Right-click > Export Items > save as .p12 with a password
  4. Base64-encode it: base64 -i DeveloperID.p12 | pbcopy
  5. Paste the result into the MACOS_CERTIFICATE_P12 GitHub secret

Behavior:

  • If MACOS_CERTIFICATE_P12 is 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)

Development

# 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.xcodeproj

See 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!

NetworkAddress
Ethereum0x30c8b011AF68a963694Ce1E5f54A545442acFEfA
TronTDAyNkS36eKyqv9s4KQpu4ebWciQ2bqdW3
Bitcoinbc1qtqm7rgma8dcgqc50lmzmyxn729yqrtf9zs7asx
SolanaA4jzTGxP7tbhyDFrcWJKdsA8v5xUwF6UzvC7RFmDRrfi
Linea0x30c8b011AF68a963694Ce1E5f54A545442acFEfA
Base0x30c8b011AF68a963694Ce1E5f54A545442acFEfA
BNB Chain0x30c8b011AF68a963694Ce1E5f54A545442acFEfA
Sei0x30c8b011AF68a963694Ce1E5f54A545442acFEfA
Polygon0x30c8b011AF68a963694Ce1E5f54A545442acFEfA
OP (Optimism)0x30c8b011AF68a963694Ce1E5f54A545442acFEfA
Arbitrum0x30c8b011AF68a963694Ce1E5f54A545442acFEfA
Fantom0x30c8b011AF68a963694Ce1E5f54A545442acFEfA

License

MIT License — free for personal and commercial use.

Acknowledgments

Inspired by Caramba Switcher and Punto Switcher. Built as a free, open-source alternative.