Localization with AutoLISP

Tuesday, January 27, 2026

It may sometimes be necessary to translate your AutoLISP programs into multiple languages.

To do this, we first need a function that splits a string into a list of substrings.

;;; Splits a string into a list of substrings
;;; string : the string to split
;;; separator : the separator character
;;; ignore-empty : if true, ignores empty substrings
(defun split-string (string separator ignore-empty / result current-word index character)
  (setq result '())
  (setq current-word "")
  (setq index 1)

  (while (<= index (strlen string))
    (setq character (substr string index 1))
    (if (= character separator)
      (progn
        (if (or (not ignore-empty) (/= current-word ""))
          (setq result (append result (list current-word)))
        )
        (setq current-word "")
      )
      (setq current-word (strcat current-word character))
    )
    (setq index (1+ index))
  )

  (if (or (not ignore-empty) (/= current-word ""))
    (setq result (append result (list current-word)))
    result
  )
)

Next, we need to know which language AutoCAD is using to determine which translations to load. Surprisingly, AutoCAD does not provide a system variable for this. There is a variable named LOCALE, but it returns a code corresponding to the operating system language. To get the language used by AutoCAD, we need to use a workaround:

;;; Returns the current AutoCAD culture in uppercase
;;; FRA for French/France, ENU for English/United States, etc.
(defun get-acad-culture (/)
  ;; The LOCALE system variable returns the operating system language, not the AutoCAD culture.
  ;; The LOCALROOTPREFIX system variable contains a path that ends with the culture code.
  ;; Example: C:\Users\<Username>\AppData\Local\Autodesk\AutoCAD 2026\R25.1\enu\
  (strcase (last (split-string (getvar "LOCALROOTPREFIX") "\\" T)))
)

The LOCALROOTPREFIX system variable contains a path that ends with the AutoCAD culture code. So we can use the first function split-string to extract the culture code.

Next, for translations, we will use a TSV (Tab-Separated Values) file. This type of file allows storing tabular data and can be easily created and edited with a spreadsheet application like Microsoft Excel.

ID	ENU	FRA
hello	Hello	Bonjour
goodbye	Goodbye	Au revoir

Be careful to use UTF-8 encoding for the TSV file, otherwise special characters (e.g. accented letters) will not display correctly.

The first column is a unique identifier for each translation. The following columns contain the translations in different languages.

The file can be loaded with the following function:

;;; Returns the index (0-based) of an element in a list, or nil if not found
;;; element : the element to search for
;;; items : the list to search in
(defun find-index (element items / index found)
  (setq index 0)
  (setq found nil)
  (while (and (not found) (nth index items))
    (if (= (nth index items) element)
      (setq found T)
      (setq index (1+ index))
    )
  )
  (if found index nil)
)

;;; Loads translations from a TSV file for the current culture
;;; The file must have a header with culture codes (ENU, FRA, etc.)
;;; The first column contains the identifiers
;;; filename : path to the TSV file
;;; Result: defines the global variable $strings (association list of dotted pairs)
(defun load-strings (filename / file header-line headers culture-index line columns)
  (setq file (open filename "r"))
  (if (not file)
    (progn
      (princ (strcat "\nError: unable to open file " filename))
      nil
    )
    (progn
      ;; Read the header and find the column for the current culture
      (setq header-line (read-line file))
      (setq headers (split-string header-line "\t" T))
      (setq culture-index (find-index (get-acad-culture) headers))

      ;; If the current culture is not found, default to ENU
      (if (not culture-index)
        (setq culture-index (find-index "ENU" headers))
      )

      (if (not culture-index)
        (progn
          (close file)
          (princ "\nError: no translation column found.")
          nil
        )
        (progn
          ;; Read each line and build the association list
          (setq $strings '())
          (while (setq line (read-line file))
            (setq columns (split-string line "\t" nil))
            (if (and (car columns) (nth culture-index columns))
              (setq $strings (cons (cons (car columns) (nth culture-index columns)) $strings))
            )
          )
          (setq $strings (reverse $strings))
          (close file)
          $strings
        )
      )
    )
  )
)

The translations are stored in the global variable $strings (note the $ which indicates a global variable). It will contain an association list of dotted pairs similar to ('("hello" "Hello") ("goodbye" "Goodbye")). This is the equivalent of a dictionary in Python or .NET.

Once the translations are loaded, you can use assoc to retrieve a translation:

;;; Returns the translation associated with an identifier
;;; identifier : the identifier of the string to translate
(defun get-string (identifier)
  (cdr (assoc identifier $strings))
)

You can now use this small framework like this:

(load-strings "translations.tsv")
(princ (get-string "hello"))

Helping hand Need an AutoCAD (AutoLISP, ObjectARX, .NET, VBA) development? Contact me for a free quote.