If you need a language reference keyboard overlay but don’t won’t to buy another keyboard or decals you can use this as a reference. Currently I include Russian, French, Spanish, and German with English as the base language. I might make other keyboards; this is for a 102 key laptop layout. I may add other languages also but it’s easy to add your own. Everything is layered for ease of use. It can be downloaded here and I recommend using Inkscape if you plan to edit. Code shows how to change apps size, keep on top, and overlay.

Made it into a Gtk 3 application. Download
Here’s the code if you prefer not downloading. It’s important that the keyboards directory is in the same directory as the script for the first run. It will copy the images and create a config in ~/.foreignkeys
The images are included in the Gtk 3 download.
#!/usr/bin/env python
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk, Gdk, Gio, GdkPixbuf
import os
import shutil
import sys
import getpass
import json
ABOUT_MSG = """ ForeignKeys: Language Keyboard Reference
Created by C. Nichols, Feb. 2022
Visit me: https://darkartistry.com
You can edit the keyboards in
~/.foreignkeys/keyboards directly in Inkscape. """
SIZES = {
'large': (1920,580),
'medium': (1440,440),
'small': (1024,305)
}
def read_conf(conf_path):
with open(conf_path, 'r') as fh:
conf = json.loads(fh.read())
return conf
def write_conf(conf_path, conf):
conf = json.dumps(conf)
with open(conf_path, 'w') as fh:
fh.write(conf)
def set_get_config(app_path):
"""Set default config."""
config_path = os.path.join(app_path, 'foreignkeys.conf')
if not os.path.exists(config_path):
conf = {'AlwaysOnTop': True, 'UserPath': app_path, 'language': 'russian', 'size': 'medium'}
write_conf(config_path, conf)
return config_path
def init(app_name='foreignkeys', user=None):
"""Create home directory for config and images."""
if not user:
print('Unable to determine user.')
sys.exit()
app_path = os.path.join('/home', user, '.%s' % app_name)
img_path = os.path.join(app_path, 'keyboards')
try:
cwd = os.getcwd()
kbp = os.path.join(cwd, 'keyboards')
if not os.path.exists(app_path):
os.mkdir(app_path)
if os.path.exists(kbp):
if not os.path.exists(img_path):
shutil.copytree(kbp, img_path)
else:
print('Missing keyboards directory and its resources.')
except:
print('Unable to create path at %s' % app_path)
sys.exit()
return app_path
def get_overlays(tp):
"""Get images for dropdown."""
kb_overlays = {}
for f in os.listdir(tp):
filename = f.split('.')[0]
fpath = os.path.join(tp, f)
kb_overlays[filename] = fpath
return kb_overlays
def get_screen_size():
"""Get the size of the physical screen."""
rects = []
display = Gdk.Display.get_default()
mcount = display.get_n_monitors()
for i in range(mcount):
rects.append(display.get_monitor(i).get_geometry())
rx = min(rec.x for rec in rects)
ry = min(rec.y for rec in rects)
wx = max(rec.x + rec.width for rec in rects)
hy = max(rec.y + rec.height for rec in rects)
width = wx - rx
height = hy - ry
return (width, height)
class MainWindow(Gtk.Window):
def __init__(self, base_path, screen_sz):
Gtk.Window.__init__(self, title="ForeignKeys")
self.set_position(Gtk.WindowPosition.CENTER)
# ********* Config *********
self.conf_path = set_get_config(base_path)
self.config = read_conf(self.conf_path)
self.checked_on_top = self.config.get('AlwaysOnTop')
self.selected_ov = self.config.get('language')
self.wind_size = self.config.get('size')
self.width = SIZES[self.wind_size.lower()][0]
self.height = SIZES[self.wind_size.lower()][-1]
self.app_path = self.config.get('UserPath')
self.keyboard_ovl = os.path.join(self.app_path, 'keyboards')
self.kb_overlays = get_overlays(self.keyboard_ovl)
# ********* Config *********
self.set_keep_above(True)
self.set_resizable(False)
self.set_default_icon_name("input-keyboard")
self.set_default_size(self.width, self.height)
# ********* Create HeaderBar **********
self.header = Gtk.HeaderBar()
self.header.set_show_close_button(True)
self.header.props.title = "ForeignKeys"
self.set_titlebar(self.header)
self.popover = Gtk.Popover()
vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
vbox.set_size_request(150, 200)
self.chk_top = Gtk.CheckButton(label="Always on top")
self.chk_top.set_active(self.checked_on_top)
self.chk_top.connect("toggled", self.on_checked)
vbox.pack_start(self.chk_top, True, False, 5)
self.sel_language = Gtk.ComboBoxText()
for ov in sorted(self.kb_overlays.keys()):
self.sel_language.append(ov.lower(), ov.title())
self.sel_language.set_active_id(self.selected_ov)
self.sel_language.connect("changed", self.on_language_changed)
vbox.pack_start(self.sel_language, True, False, 5)
self.sel_size = Gtk.ComboBoxText()
self.sel_size.append('large', 'Large')
self.sel_size.append('medium', 'Medium')
self.sel_size.append('small', 'Small')
self.sel_size.set_active_id(self.wind_size.lower())
self.sel_size.connect("changed", self.on_size_changed)
vbox.pack_start(self.sel_size, True, False, 5)
btn_about = Gtk.Button(label="About")
btn_about.connect("clicked", self.on_about_click)
vbox.pack_start(btn_about, True, False, 5)
separator = Gtk.Separator(orientation=Gtk.Orientation.HORIZONTAL)
vbox.pack_start(separator, True, False, 5)
btn_quit = Gtk.Button(label="Quit")
btn_quit.connect("clicked", self.on_quit_click)
vbox.pack_start(btn_quit, True, False, 5)
vbox.show_all()
self.popover.add(vbox)
self.popover.set_position(Gtk.PositionType.BOTTOM)
mnu_button = Gtk.MenuButton(popover=self.popover)
mnu_icon = Gio.ThemedIcon(name="open-menu-symbolic")
mnu_image = Gtk.Image.new_from_gicon(mnu_icon, Gtk.IconSize.BUTTON)
mnu_button.add(mnu_image)
self.header.pack_end(mnu_button)
# Create Box
mnu_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
Gtk.StyleContext.add_class(mnu_box.get_style_context(), "linked")
self.header.pack_start(mnu_box)
# ********* Create HeaderBar **********
self.overlay = Gtk.Overlay()
self.add(self.overlay)
pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_size(self.kb_overlays[self.selected_ov], self.width, self.height)
self.background = Gtk.Image.new_from_pixbuf(pixbuf)
self.overlay.add(self.background)
def on_checked(self, widget):
state = self.chk_top.get_active()
self.set_keep_above(state)
self.show_all()
self.config['AlwaysOnTop'] = state
write_conf(self.conf_path, self.config)
def on_size_changed(self, widget):
switch_size = self.sel_size.get_active_text().lower()
self.width = SIZES[switch_size.lower()][0]
self.height = SIZES[switch_size.lower()][-1]
self.set_size_request(self.width, self.height)
self.overlay.remove(self.background)
pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_size(self.kb_overlays[self.selected_ov], self.width, self.height)
self.background = Gtk.Image.new_from_pixbuf(pixbuf)
self.overlay.add(self.background)
self.overlay.show_all()
self.show_all()
self.config['size'] = switch_size
write_conf(self.conf_path, self.config)
def on_language_changed(self, widget):
self.overlay.remove(self.background)
switch_lang = self.sel_language.get_active_text().lower()
pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_size(self.kb_overlays[switch_lang.lower()], self.width, self.height)
self.background = Gtk.Image.new_from_pixbuf(pixbuf)
self.overlay.add(self.background)
self.overlay.show_all()
self.show_all()
self.config['language'] = switch_lang
write_conf(self.conf_path, self.config)
def on_about_click(self, widget):
dialog = About(self)
response = dialog.run()
if response == Gtk.ResponseType.OK:
dialog.destroy()
def on_quit_click(self, widget):
Gtk.main_quit()
class About(Gtk.Dialog):
def __init__(self, parent):
super().__init__(title="About", transient_for=parent, flags=0)
self.add_buttons(Gtk.STOCK_OK, Gtk.ResponseType.OK)
self.set_default_size(190, 200)
label = Gtk.Label(label=ABOUT_MSG)
box = self.get_content_area()
box.add(label)
self.show_all()
if __name__ == '__main__':
screen_size = get_screen_size()
user_path = init(user=getpass.getuser())
window = MainWindow(base_path=user_path, screen_sz=screen_size)
window.connect('delete-event', Gtk.main_quit)
window.show_all()
Gtk.main()See it in action. You might want to change the video to high quality in the player or watch at YouTube as it sucks by default.
