Java .properties vs .NET .resx: two long-lived resource formats compared
Some file formats are new and change every year; others have been sitting quietly underneath enterprise software for decades, doing the same job the same way. Java's .properties format and .NET's .resx format are both in the second category. Neither is fashionable, both are still exactly what you will find in a large share of desktop, server, and enterprise codebases, and both encode localizable text with different tradeoffs worth knowing before you touch either one.
Java .properties: key=value, plain text
A .properties file is about as simple as a text-based resource format gets: one key-value pair per line, separated by an equals sign or a colon, with a backslash used to escape special characters and to continue a value across multiple lines.
# Login screen
welcome.title=Welcome back
logout.confirm=Are you sure you want to log out?
help.text=This is a long line that continues \
onto the next physical line in the file.Translation works the same way it does for most resource formats: one file per language, with the language encoded in the filename — messages.properties for the default, messages_de.properties for German, messages_fr_CA.properties for Canadian French — and the application picks the matching file, falling back toward the base file when a more specific one or a specific key is missing.
Escaping and the encoding history that still matters
Because the format has no quoting mechanism, a handful of characters need a backslash escape to be usable inside a value: the equals sign and colon (which would otherwise look like the key-value separator), the backslash itself, and line breaks for multi-line values as shown above. Unicode characters outside the format's original assumptions are also commonly written as \uXXXX escapes rather than as literal bytes.
That last point traces back to a real historical constraint: .properties files were long specified to be read as Latin-1 (ISO-8859-1) by default, a single-byte encoding that cannot represent most of the world's scripts directly. Text outside that range had to be escaped as \uXXXX sequences instead of written as literal characters, which is why older tooling and older codebases still contain files full of escape sequences that look unreadable in a plain text editor. Modern tooling generally reads and writes .properties files as UTF-8, which lets translated text appear as literal characters rather than escape sequences, but a mismatch between what a build step assumes and what a file actually contains is a durable source of mojibake in older projects — worth checking explicitly rather than assuming it is fine.
.NET .resx: XML, structured, with more than just text
A .resx file is XML, and it carries more than a flat set of strings: each entry is a data element identified by a name attribute, containing a value element for the actual content and an optional comment element for translator context — closer in spirit to Apple's commented .strings entries than to a bare .properties line.
<data name="WelcomeTitle" xml:space="preserve"> <value>Welcome back</value> <comment>Shown on the home screen after login</comment> </data> <data name="LogoutConfirm" xml:space="preserve"> <value>Are you sure you want to log out?</value> </data>
Because it is a full XML-based resource container rather than a plain text list, a .resx file can hold more than strings — embedded images and other binary resources are valid entries too, base64-encoded inside the XML. In practice, localization work only ever touches the string entries; binary resources are a reminder that .resx is a general-purpose resource format Microsoft reused for text, not a format designed for translation first.
The naming convention follows the same pattern as the other two formats in this comparison: a base Resources.resx, then culture-specific files like Resources.de.resx or Resources.fr-CA.resx sitting alongside it. At build time, .NET compiles each culture file into a satellite assembly — a separate DLL containing only that culture's resources, placed in a subdirectory named after the culture code next to the main application. This is a meaningfully different distribution model from the other formats covered here: translations are not just alternate files read at runtime, they are compiled artifacts the application loads on demand once it knows which culture it needs.
Where each format bites
Both formats have failure modes that are easy to trigger and easy to miss:
- .properties: an unescaped equals sign or colon inside a value silently truncates the key, producing a key nobody meant to create rather than an error
- .properties: encoding mismatches between what a tool reads and what a file actually contains turn accented or non-Latin characters into garbled text without any warning
- .resx: forgetting the xml:space preserve attribute lets an XML parser collapse meaningful leading or trailing whitespace in a value
- .resx: a malformed data element (unclosed tag, invalid name) can fail the whole file to parse, unlike a bad line in a .properties file which usually just corrupts one entry
- .resx: satellite assemblies mean a missing culture file is a missing DLL at deploy time, not just a missing text file — deployment scripts have to know about it, not only the build
What they have in common
Underneath the syntax difference, both formats solve the identical problem the same way: a stable key referenced from code, one file per language sharing the same set of keys, and a fallback to a base language when a specific translation is missing. Both also share the same operational risk — because the fallback is silent in both formats, a missing key or a missing language file does not surface as an error, only as untranslated text a user eventually notices. Whichever format a codebase uses, checking key parity across all language files is the same discipline either way.