Simple support for user-interface languages in AutoHotkey
By raffriff42
![]()
Open Source under GNU Lesser General Public License - see LEGAL section. This page updated 2025-11-21
[TOC]
OneLocale provides an easier way to support multiple user-interface languages in AutoHotkey. The developer (you) creates rough user-interface text ad-hoc and in-place, in your preferred language. You don’t need to bother with the final user-interface text until later.
OneLocale loads the language-specific text at runtime. There is one .LANG file for each supported language.
Translators do not need to know AutoHotkey. They can create a translation without interacting with you, the developer.
Even if you don’t plan to support multiple languages, the way OneLocale helps distinguish user-interface text from other string literals in your code is very valuable for code maintenance.
Is a .GIF worth 1000 words?
Here are some examples to get us started:
Here’s a sample script fragment adding a Button:
; MyScript.ahk
G := Gui()
btn_Help := G.Add("Button", "w120", "Help..."))
btn_Help.ToolTip := "View Readme in browser."
Here’s the same script fragment using OneLocale:
; MyScript.ahk - using OneLocale
G := Gui()
btn_Help := G.Add("Button", "w120", sT("gui", "btn_Help", "/Help..."))
; st( ^section ^key ^default)
btn_Help.ToolTip := sT("tooltips", "btn_Help", "/View Readme in browser.")
; st( ^section ^key ^default )
An English .LANG file might have these entries for this control:
; MyScript-[en].lang
[gui]
btn_Help = &Help
[tooltips]
btn_Help = View the Readme file.
And a German .LANG file might have these entries:
; MyScript-[de].lang
[gui]
btn_Help = &Helfen
[tooltips]
btn_Help = Lese die readme Datei.
The Button text will depend on which .LANG file is loaded.

…and obviously, the tooltip will work the same way.
Here’s a sample script fragment adding a Menu item:
; MyScript.ahk
FileMenu := Menu()
FileMenu.Add(mnu_lang := "&Language...`tCtrl+L", MenuHandler)
Here’s the same script fragment using OneLocale:
; MyScript.ahk - using OneLocale
FileMenu := Menu()
FileMenu.Add(mnu_lang := sT("menu", "language", "/&Language...\tCtrl+L"), MenuHandler)
; st( ^section ^key ^default )
The .LANG files might look like this:
; MyScript-[en].lang
[menu]
language = &Language...\tCtrl+L
file_test = &Generate error message\tCtrl+E
file_quit = &Quit\tCtrl+Q
; MyScript-[de].lang
[menu]
language = &Sprache...\tCtrl+S
file_test = &Fehlermeldung generieren\tCtrl+F
file_quit = &Beenden\tCtrl+B
Here how the menus look in the running application:

Note
Ampersand (&) sets the Access Key.
Tab (\t) separates menu text and an optional Accelerator Key.
Ctrl, Alt, and/or Shift (eg, Ctrl+S).Win key could be used, but is normally for system-wide actions.; MyScript.ahk
filename := "some-file-name.txt"
msg := "File " filename " not found."
MsgBox(msg)
; MyScript.ahk - using OneLocale
filename := "some-file-name.txt"
msg := sT("errors", "bad_path", "/File %path% not found.", {path:filename})
; st( ^section ^key ^default { ^args })
MsgBox(msg)
We’re using sT()’s fourth argument now - args, an Object literal with names and values to support variable expansion. Variable expansion (%path% → “some-file-name.txt”) is a useful feature even if you don’t plan to support multiple languages.
The .LANG files might look like this:
; MyScript-[en].lang
[errors]
bad_path = The file '%path%' could not be found.
; MyScript-[de].lang
[errors]
bad_path = Die Datei '%path%' konnte nicht gefunden werden.
The output will depend on which .LANG file is loaded: with MyScript-[en].lang loaded, the output of this example is
bad_path: The file ‘some-file-name.txt’ could not be found.
And with MyScript-[de].lang loaded, the output is
bad_path: Die Datei ‘some-file-name.txt’ konnte nicht gefunden werden.
Note how the key bad_path is shown to the user verbatim; this feature (which works for the errors section only) allows tech support to identify the error by a string that does not change, regardless of the user’s language. When a user from the other side of the world posts a screen shot, tech support will know which error was triggered.
The heart of OneLocale is the sT() function, which we saw in the examples above. Here it is again:
msg := sT("errors" ; section
, "bad_path" ; key
, "/File %path% not found." ; default
, {path:filename} ; args object
, langPath:="") ; lang path override
The first argument (errors) is the .LANG file section. You can create whatever sections you like; I like to use gui, tooltips, errors and sections for each child window.
The second argument (bad_path) is the key or name of a particular string.
The third argument is the default value, which is used if the requested key doesn’t have a value.
A default is useful during initial development as you can see something on the screen, even if the .LANG file is incomplete.
It also serves as a reminder to you, the developer, of the function of this piece of text (and the Gui control or MsgBox it is associated with), and also serves as a starting point for the .LANG file.
I like to prefix the default with
/as shown, to make it obvious when there is a problem.
The fourth argument is an Object literal to support the variable expansion mentioned above.
The fifth argument is for edge cases where you might want to load a second .LANG file for certain strings — perhaps to use sT()’s variable expansion on non-UI text, such as a list of command-line templates. If this argument is empty (the default), the .LANG file is pointed to by g_lang_path.
sT() requires the global variable g_lang_path, which you must define. g_lang_path avoids having to give sT() the .LANG path as an argument every time, which would add to code clutter. See the code Example under Script Initialization.
If you #Include “_OneLocale_Dialog.ahk”, your users can select their language with a dialog box — a Language Chooser. It looks in your Language directory for all matching .LANG files — so if your user downloads one from the Internet and puts it in that folder, the dialog loads it into a Listbox:

If you #Include “_OneLocale_Utils.ahk” you get helpful routines to localize things like Time, Date, Currency & Number formats.
The functions are listed below.
Your script needs an .INI file. It probably has one already. To make use of OneLocale, it requires the following section and entry:
; MyScript.ini
[general]
language = (a Language ID)
The Language ID can be any name you like; ISO 639-1 2-letter codes are suggested (en for English, fr for French, de for German, etc.). To distinguish British English from American English, or Simplified Chinese from Chinese for example, use Microsoft’s language tag table. You can also use LCID codes such as returned by A_Language, but they’re not as readable for humans.
OneLocale needs to be included in your script.
; MyScript.ahk
; language support
Include "_OneLocale.ahk" ; required
Include "_OneLocale_Dialog.ahk" ; optional
;#Include "_OneLocale_Utils.ahk" ; optional
The recommended directory structure is this:
Script-Folder
|-- Lang
|-- Docs
| |-- assets
Script-Folder’ contains your .AHK and/or .EXE, your .INI, etc.Lang’ contains .LANG files. It can contain other files too if you want.Docs’ is for documentation — .TXT, .MD, .HTML, etc. Images and .CSS files
should — but aren’t required to — go in a subfolder like ‘Docs\assets’.If you want, you can put all the files just mentioned directly in the Script folder; we’ll see how in the next section.
OneLocale_Init() must be inserted in the script before any sT() calls.
; MyScript.ahk
;OneLocale_Init(optional_args:="")
locale_info := OneLocale_Init()
if (!locale_info.success) {
MsgBox(locale_info.errmsg, S_TITLE, "icon!")
ExitApp
}
g_lang_path := locale_info.langPath ; 'g_lang_path' is required by sT()
g_ini_path := locale_info.iniPath ; optional - for your convenience
g_html_path := locale_info.docsPath ; '' '' '' ''
The default arguments suffice IF ( assuming MyScript is replaced with your script name, and /TAG/ means the active language, like “en” )
MyScript.ini and is located in A_ScriptDirMyScript-[/TAG/].lang and is located in
A_ScriptDir \langMyScript-[/TAG/].html and is located in
A_ScriptDir \docsA_LanguageIf you want your .INI to also hold your UI text, eliminating .LANG files (most useful for very simple scripts where you don’t want to bother with a separate .LANG file just yet, but want be ready for future expansion), you would set noLangFile (an optional argument) to true.
locale_info := OneLocale_Init( {noLangFile:true} )
See Example - no .LANG File below.
If you want to put the help file in A_ScriptDir, eliminating the
“\docs” subdirectory, you would clear sDocsFolder:
locale_info := OneLocale_Init( {sDocsFolder:""} )
If your Script name (A_ScriptName without the extension) doesn’t match your .INI file name, you would set sName. This sets the name of your .INI, and the base names of your .LANG and documentation files. Useful if you want to try a temporary script like MyScript_TEST.ahk
locale_info := OneLocale_Init( {sName:"MyScript"} )
If your help file is not named MyScript-[/TAG/].htmml as described above, like README-/TAG/.asc for example, you would set sDocName and sDocExt:
locale_info := OneLocale_Init( {sDocName:"README-/TAG/", sDocExt:"asc"} )
If your documentation is in one language only, don’t incude /TAG/:
locale_info := OneLocale_Init( {sDocName:"README", sDocExt:"asc"} )
OneLocale requires a .LANG file: if the Language ID is “en”, and your script is named MyScript, by default OneLocale_Init() would look for a file named MyScript-[en].lang. Make sure the Language ID in your .INI file matches the language code in your .LANG file’s name.
A .LANG file is actually an .ini file with a different extension. This lets you set up custom file associations and editor preferences for both types of file. All .INI and .LANG files are accessed with AutoHotkey IniRead. You should familiarize yourself with its ‘quirks and features’ — most importantly that you must save a .LANG file as UTF-16 with BOM (Byte Order Mark) to preserve Unicode characters.
You don’t need to add anything to the file for now. Your script will work just fine with an empty .LANG file IF you have set default UI text in your code as we showed earlier.
Here is where you go through your code and locate all the user-interface text bits (window and button captions, text controls, checkbox labels, error messages, …) and wrap them in sT() calls:
;; before
ctlChk1 := G.Add("CheckBox", , "&Option 1")
;; after
ctlChk1 := G.Add("CheckBox", , sT("gui", "ctlChk1", "/&Option 1"))
;; ^SECTION ^KEY ^DEFAULT VALUE
You need to tell sT() the section and the key. (Maybe take another look at the GIF).
Common section names are gui (GUI elements in the main window), tooltips (tooltips in the main window), and errors (error messages).
Each child window should have its own sections, like
dialog1_gui,dialog1_tooltipsand maybedialog1_errors. It’s up to you.
The key names are generally the same or close to the control’s name, like in this example. You can re-use the same key names in different sections.
Then you simply copy your existing UI text into the Default argument. Again, I like to prefix the default with / as shown, to make it obvious when there is a problem, like a missing section/key/value in the .LANG file — and they will ALL be missing at this point, because your .LANG file is just a stub.
Run your script, and all your UI text will appear just as it was but with / prefixes, showing that OneLocale is working, but there is no .LANG data as yet.
This part is surprisingly easy - the information you need to build the .LANG file is in your code. All you need to do is copy and paste. (It goes so quickly that I’m not very motivated to write something to automate the process.)
; MyScript.ahk
ctlChk1 := G.Add("CheckBox",
, sT("gui", "ctlChk1", "/&Option 1"))
; MyScript-[en].lang
[gui]
ctlChk1 = &Option 1
For messages with line breaks, you can insert \n’s as needed. However, longer messages get a bit unwieldy. For example (from something I’m working on):
;; Long message method #1 - get key value
[refresh]
Failed = THERE MAY BE A PROBLEM: \n\n You requested %req_fps%Hz refresh rate, but actual \n refresh rate is %new_fps%Hz.
You may sometimes want to give those messages their own sections:
;; Long message method #2 - get entire section
[refresh_failed]
THERE MAY BE A PROBLEM: \n
You requested %req_fps%Hz refresh rate, but actual
refresh rate is %new_fps%Hz.
The output from the above (using either method, and given appropriate data for req_fps and new_fps) looks like this:
THERE MAY BE A PROBLEM:
You requested 90Hz refresh rate, but actual refresh rate is 60Hz.
The second .LANG file entry is easier to write as it looks more like the final output.
If you want a line break in your .LANG file but not in your output, use \w at the end of the line (this lets the Gui element handle Word Wrap)
Okay, you’re basically done. You have a complete .LANG file! Now you can:
sT()’s variable expansion.(1) Open the first .LANG file, save with a new name
Name must be of the form ‘MyScript-[XX].lang’, where XX is a language identifier. This language identifier can be anything you like, but ISO 639-1 codes are suggested (en for English, fr for French, de for German, etc). For sublanguages (American English, Simplified Chinese), use Microsoft’s language tag table.
Some examples:
‘MyScript-[fr].lang‘ (French) ‘MyScript-[de].lang‘ (German) ‘MyScript-[es].lang‘ (Spanish) ‘MyScript-[en-gb].lang’ (British English) ‘MyScript-[en-us].lang’ (American English)
(2) Translate the user-interface strings
For example, let’s translate a .LANG file section to French. I’m using Google Translate (this paragraph was written in 2023) because I don’t speak the language. Note, some rephrasing and abbreviations are used to control length.
:point_right::point_right: In 2025, it’s preferable to use an AI to do your translations. Once they understand the rules, they can handle a whole .LANG file at once instead of line-by-line.
\z which starts a comment to the translator. You may add your own comments. This text will not appear to the user.% marks.Translate only the part to the right of the = (equals) sign —
except for Multi-line sections.
= on every line like single-line sections.\t (Tabs) - spaces won’t work.\n (newlines)Try not to let translated strings get much longer than the originals. (this may or may not be important, depending on the UI)
; MyScript-[en].lang (INPUT)
[gui]
Prompt = &Select new Resolution:
Mode_Native = (native) \z(Native resolution for a monitor)
Button_OK = OK
Button_Cancel = Cancel
Post_Change = Monitor %id% New Resolution %wid% x %hgt% @ %refresh%
; MyScript-[fr].lang (OUTPUT)
[gui]
Prompt = &Nouvelle résolution:
Mode_Native = (natif)
Button_OK = OK \z Default button - no access key needed
Button_Cancel = Annuler \z Cancel button - no access key needed
Post_Change = Moniteur %id% nouvelle rés. %wid% × %hgt% @ %refresh%
(3) Save the file as UTF-16 with BOM (Byte Order Mark)
To use the new language file, place it in your .LANG folder and either update Language= in MyScript.ini to your language code, or if you have implemented the Language Dialog, let your users select it themselves.
These notes are summarized in the short document OneLocale - Notes for Translators.
OneLocale was first created in February 2023, and has been slowly refined as it was used in many of my personal projects.
It is a complete rework of my 2013 NetText (sourceforge), which was based on my 2011 GetText (sourceforge), which was inspired by GNU’s 1995 gettext (gnu.org).
OneLocale™ is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2.1 of the License, or (at your option) any later version.
This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
I believe that unless otherwise attributed in the source, this AutoHotkey code is original (other than sample code from the Documentation)
Documentation © 2025 raffriff42 CC BY 4.0
![]()
Any sample code demonstrating usage is public domain, marked CC0 1.0
Icon adapted from Wikimedia Commons Earth_icon_2.png, marked CC0 1.0
(end)