Localise an Excel Report for Other Languages and Locales
A report that works for readers in London often goes wrong in Berlin or Paris, and the usual mistake is made with good intentions: formatting numbers and dates as strings in Python, in the reader's style, before writing them. The cell then shows 1.234.567,89 as text, cannot be summed, and looks wrong to anyone with different settings. Excel already localises a great deal by itself if the file is written the right way. This page shows what Excel adapts automatically, what you must pin explicitly, and how to check the result under several locales without owning several computers. It is part of Applying Number and Date Formats in Excel.
Prerequisites
pip install openpyxl pandas
The checks render the workbook with LibreOffice under different locale settings, using the LC_ALL environment variable, and read the result with pdftotext. Excel applies the same rules from the reader's Windows or macOS regional settings.
Step 1 — What adapts by itself
A number format code in an .xlsx file is locale-neutral: in #,##0.00, the comma means "thousands separator" and the dot means "decimal separator". The reader's application substitutes its own characters. The same goes for the built-in short date, which openpyxl calls mm-dd-yy (format number 14): it is not a fixed pattern but "the reader's short date". Write a small sheet and render it under three locales:
import datetime as dt
from openpyxl import Workbook
wb = Workbook()
ws = wb.active
ws.append(["Label", "Value", "Format"])
rows = [
("Revenue", 1234567.891, "#,##0.00"),
("Short date", dt.date(2026, 10, 5), "mm-dd-yy"),
("Month", dt.date(2026, 10, 5), "mmmm yyyy"),
("Fixed pattern", dt.date(2026, 10, 5), "dd/mm/yyyy"),
]
for label, value, fmt in rows:
ws.append([label, value, fmt])
ws.cell(row=ws.max_row, column=2).number_format = fmt
ws.column_dimensions["A"].width = 16
ws.column_dimensions["B"].width = 22
wb.save("neutral.xlsx")
for loc in en_US de_DE fr_FR; do
mkdir -p "$loc"
LC_ALL=$loc.UTF-8 soffice -env:UserInstallation=file://$PWD/lo_$loc --headless --convert-to pdf --outdir "$loc" neutral.xlsx > /dev/null 2>&1
echo "== $loc"; pdftotext -layout "$loc/neutral.pdf" - | sed -n '2,5p'
done
From one file: 1,234,567.89, 1.234.567,89 and 1 234 567,89; the short date as 10/5/2026, 05.10.2026 and 05/10/2026; the month as October, Oktober, octobre. Even dd/mm/yyyy changed in German, because / in a date format stands for "the date separator". Nothing in the file had to change.
Step 2 — Never pre-format numbers and dates as text
The consequence of Step 1 is the most important rule: write numbers and dates as values with format codes, never as formatted strings. A string formatted for Germany is wrong for everyone else and useless for calculation:
from openpyxl import load_workbook
wb = Workbook()
ws = wb.active
ws["A1"] = f"{1234567.891:,.2f}".replace(",", "X").replace(".", ",").replace("X", ".") # the tempting mistake
ws["A2"] = 1234567.891
ws["A2"].number_format = "#,##0.00"
ws["A3"] = "=SUM(A1:A2)"
wb.save("strings_vs_values.xlsx")
print({c: (repr(ws[c].value), ws[c].data_type) for c in ("A1", "A2")})
A1 is text and SUM ignores it; A2 is a number that every reader sees in their own format. Fix "Number Stored as Text" Warnings in Excel Output covers cleaning up files where this has already happened.
Step 3 — Pin the language when it must not follow the reader
Sometimes the report's language is fixed: a German-language report should say "Oktober" even when opened on an English computer. Prefix the format with a locale tag, [$-407] for German, [$-40C] for French, [$-415] for Polish; the number is the Windows locale ID in hexadecimal:
LOCALES = {"de": "407", "fr": "40C", "pl": "415"}
wb = Workbook()
ws = wb.active
for row, (lang, lcid) in enumerate(LOCALES.items(), start=1):
ws.cell(row=row, column=1, value=lang)
cell = ws.cell(row=row, column=2, value=dt.date(2026, 10, 5))
cell.number_format = f"[$-{lcid}]dddd d mmmm yyyy"
ws.cell(row=4, column=1, value="de-DE tag")
ws.cell(row=4, column=2, value=dt.date(2026, 10, 5)).number_format = "[$-de-DE]dddd d mmmm yyyy"
ws.column_dimensions["B"].width = 34
wb.save("pinned.xlsx")
LC_ALL=en_US.UTF-8 soffice -env:UserInstallation=file://$PWD/lo_en_US --headless --convert-to pdf pinned.xlsx > /dev/null 2>&1
pdftotext -layout pinned.pdf - | sed -n '1,4p'
Rendered on an English system, the month and day names are German, French and Polish, and the tag handles grammar too: Polish uses the genitive października after a day number, not the nominative październik of a month on its own. That is one more reason to let the format do the work instead of building date strings from a list of month names in Python. Use the hexadecimal form: the fourth row uses a tag written as a language name, [$-de-DE], and LibreOffice did not understand it, showing the raw date serial number instead of a date.
Step 4 — Currency symbols
Currency formats put the symbol in a bracketed block with its own locale ID, which also decides whether it goes before or after the number in the format code you write:
wb = Workbook()
ws = wb.active
formats = {"EUR, German style": '#,##0.00 [$€-407]', "EUR, French style": '#,##0.00 [$€-40C]',
"PLN": '#,##0.00 [$zł-415]', "GBP": '[$£-809]#,##0.00'}
for row, (label, fmt) in enumerate(formats.items(), start=1):
ws.cell(row=row, column=1, value=label)
ws.cell(row=row, column=2, value=1234567.891).number_format = fmt
ws.column_dimensions["A"].width = 20
ws.column_dimensions["B"].width = 20
wb.save("currency.xlsx")
LC_ALL=de_DE.UTF-8 soffice -env:UserInstallation=file://$PWD/lo_de_DE --headless --convert-to pdf currency.xlsx > /dev/null 2>&1
pdftotext -layout currency.pdf - | sed -n '1,4p'
The separators still follow the reader (German here); the symbol and its position are fixed by the format. Format Excel Cells as Currency with Python goes through negative amounts and accounting alignment.
Step 5 — Translate the labels from one table
Headers, sheet names and notes are text, and nothing translates them for you. Keep every label in one table keyed by an internal name, and build each language's report from the same data:
import pandas as pd
LABELS = {
"sheet": {"en": "Sales", "de": "Umsatz", "fr": "Ventes"},
"region": {"en": "Region", "de": "Region", "fr": "Région"},
"revenue": {"en": "Revenue", "de": "Umsatz", "fr": "Chiffre d'affaires"},
"month": {"en": "Month", "de": "Monat", "fr": "Mois"},
}
data = pd.DataFrame({"region": ["North", "South"], "month": pd.to_datetime(["2026-10-01", "2026-10-01"]),
"revenue": [48250.5, 39120.0]})
for lang in ("en", "de", "fr"):
frame = data.rename(columns={k: LABELS[k][lang] for k in data.columns})
with pd.ExcelWriter(f"sales_{lang}.xlsx", engine="xlsxwriter", datetime_format="mmmm yyyy") as writer:
frame.to_excel(writer, index=False, sheet_name=LABELS["sheet"][lang])
ws = writer.sheets[LABELS["sheet"][lang]]
ws.set_column(1, 1, 16)
ws.set_column(2, 2, 18, writer.book.add_format({"num_format": "#,##0.00"}))
with pd.ExcelFile(f"sales_{lang}.xlsx") as book:
print(lang, book.sheet_names, list(frame.columns))
Missing translations are the common failure: a new column is added to the data and the label table is not updated. A KeyError in the rename is the good outcome; to catch it before a release, check that every key has every language:
missing = [(key, lang) for key, names in LABELS.items() for lang in ("en", "de", "fr") if not names.get(lang)]
print("missing translations:", missing or "none")
Data values such as region names are translated the same way if the audience needs it; keep the untranslated key in a hidden column if the report is ever read back by a program.
Things that do not change with the locale
- Formulas. Files always store English function names with commas as argument separators; Excel shows them translated. Write
=SUMME(A1;A2)and the file is broken. See Fix #NAME? in Formulas Written from Python (XLOOKUP, TEXTJOIN, IFS). - Right-to-left languages. For Arabic or Hebrew reports, set
ws.sheet_view.rightToLeft = Trueso column A is on the right. - Literal separators. To force a slash whatever the locale, escape it:
dd\/mm\/yyyy.
Common pitfalls
- Formatting in Python. Strings like
1.234,56cannot be summed and look wrong elsewhere; write values with format codes. - Fixed date patterns for international readers.
dd/mm/yyyyconfuses American readers; the short date follows each reader. - Language-name tags. Use hexadecimal LCIDs such as
[$-407], which were understood in testing. - Translated formulas. Formulas in the file are always English.
- Testing in one locale. Render under the target locales, as above, before sending.
Conclusion
Write numbers and dates as values with format codes: separators, the built-in short date and untagged month names follow each reader automatically. Pin month and day names with LCID tags such as [$-407] when the report's language is fixed, put currency symbols in bracketed blocks, translate labels from one table, and remember that formulas in files are always English. Render the workbook under each target locale with LibreOffice and LC_ALL to check it.
Frequently asked questions
How do I use a comma as decimal separator in Excel from Python? You don't need to. Write numbers as values with a format such as #,##0.00; Excel shows the reader's own decimal and thousands separators.
How do I show German month names regardless of the reader's settings? Prefix the date format with the German locale ID: $-407mmmm yyyy. French is $-40C, Polish $-415.
Which date format adapts to the reader's locale? The built-in short date, format number 14, which openpyxl exposes as mm-dd-yy. It displays as 10/5/2026 in the US and 05.10.2026 in Germany.
Do I need to translate formulas for other languages? No. Formulas are stored in English with comma separators in every file. Excel translates them for display.
Related
- Up one level: Applying Number and Date Formats in Excel — the format codes in detail.
- Format Dates in Excel Cells with Python — date codes beyond the short date.
- Format Excel Cells as Currency with Python — currency and accounting formats.
- Write Custom Number Format Codes in Excel with Python — the format code syntax.
- Make an Excel Report Accessible — another way of reaching more readers.
More on number & date formats
- How-toConvert Currency Strings Like "$1,234.56" and "(500)" to Numbers with pandas
- How-toConvert Percentage Strings Like "12.5%" to Numbers with pandas
- How-toExcel TEXT Function Equivalent in pandas
- How-toMatch Excel Rounding in pandas
- How-toConvert Excel Time Values and Durations in Python
- How-toAdd Variance Arrows and Trend Indicators to a Dashboard