Skip to content

feat: add .properties file format plugin#4345

Closed
nbouvrette wants to merge 6 commits into
opral:mainfrom
nbouvrette:feat/plugin-properties-file
Closed

feat: add .properties file format plugin#4345
nbouvrette wants to merge 6 commits into
opral:mainfrom
nbouvrette:feat/plugin-properties-file

Conversation

@nbouvrette
Copy link
Copy Markdown
Contributor

@nbouvrette nbouvrette commented Apr 12, 2026

Summary

Adds @inlang/plugin-properties-file — a plugin that enables using Java .properties files for storing translations in inlang/Paraglide projects.

  • Built on the properties-file npm package for robust parsing
  • Variable interpolation with {variable} syntax (matching inlang message format conventions)
  • Proper escaping of control characters (\n, \r, \t, \\) in exported values
  • Explicit error on multi-variant messages (plural/select not supported by .properties format)
  • Optional key sorting (ascending/descending)
  • 21 tests covering roundtrip integrity, multiple locales, special characters, unicode, escaping, and edge cases

Why .properties?

The .properties format is universally supported by every major TMS (Crowdin, Phrase, Lokalise, Transifex) — avoiding format lock-in. It also provides inline comments for translator context, a feature unavailable in JSON-based formats:

# This heading appears above the fold — keep it under 60 characters
hero.title = The End of Coding
# Shown below the title, can be longer
hero.subtitle = Building AI-Native Organizations When Humans No Longer Write Code

Configuration

{
  "modules": ["@inlang/plugin-properties-file"],
  "plugin.inlang.propertiesFile": {
    "pathPattern": "./messages/{locale}.properties"
  }
}

CI Note

The @inlang/website-v2:lint failure is a pre-existing issue on the main branch (unused imports/declarations from the recent landing page rewrite). A separate fix has been submitted in #4346.

Future work (not in this PR)

  • Co-located file support: Component-scoped .properties files (e.g., ./src/components/Hero/{locale}.properties) via glob-based path patterns. This could eventually become a shared utility across format plugins.
  • Comment round-trip preservation: Currently comments are preserved in source files but not through SDK-driven rewrites (e.g., Fink editor). Would benefit from a metadata field on the Bundle/Message types.

Test plan

  • 21 vitest tests passing (roundtrip, variables, sorting, special characters, unicode, escaping, multi-variant rejection, edge cases)
  • tsc --noEmit passes
  • pnpm run build produces dist/index.js
  • Plugin follows existing plugin conventions (package.json scripts, esbuild config, TypeBox settings schema, marketplace manifest)

🤖 Generated with Claude Code

Add @inlang/plugin-properties-file — a plugin that enables using Java
.properties files for storing translations in inlang/Paraglide projects.

Built on the `properties-file` npm package (https://github.com/properties-file/properties-file)
for robust parsing of the .properties format including Unicode escapes,
multiline values, and special characters.

Features:
- Import/export of .properties files (key = value format)
- Variable interpolation with {variable} syntax
- Inline comment support (comments preserved in source files)
- Optional key sorting (ascending/descending)
- Multiple path patterns for organizing translation files

Configuration:
```json
{
  "modules": ["@inlang/plugin-properties-file"],
  "plugin.inlang.propertiesFile": {
    "pathPattern": "./messages/{locale}.properties"
  }
}
```

The .properties format is universally supported by every major TMS
(Crowdin, Phrase, Lokalise, Transifex) and provides inline comments
for translator context — a feature unavailable in JSON-based formats.

19 tests covering simple values, variables, multiple locales, roundtrip
integrity, key sorting, special characters, unicode, and edge cases.
@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented Apr 12, 2026

🦋 Changeset detected

Latest commit: 8d3dec1

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@inlang/plugin-properties-file Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Apr 12, 2026

CLA Assistant Lite bot All contributors have signed the CLA ✍️ ✅

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c493bc634b

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".


const lines: string[] = [];
for (const entry of entries) {
lines.push(`${entry.key} = ${entry.value}`);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Escape serialized property values

The exporter writes raw text directly into .properties lines (key = value) without escaping control characters, so values containing newlines, trailing backslashes, or other syntax-significant characters are emitted as invalid/misparsed properties content. In practice this corrupts messages on roundtrip (for example, a pattern containing \n becomes multiple lines and changes key/value boundaries), so serialization needs proper .properties escaping instead of plain string concatenation.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in a295a33 — exported values now escape backslashes, newlines, carriage returns, and tabs via a dedicated escapePropertyValue() function. Added a test covering control character escaping through roundtrip.

Comment on lines +30 to +32
// Properties files only support single variants (no selectors)
const variant = variantsOfMessage[0];
if (!variant) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Reject multi-variant messages instead of truncating

For messages with selectors (plural/context/etc.), variants.filter(...) can return multiple variants but the exporter unconditionally picks variantsOfMessage[0] and drops the rest. This silently loses translations when saving any project that has more than one variant per message, so the code should either serialize all variants in a defined way or throw an explicit unsupported-feature error rather than exporting incomplete data.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

@nbouvrette nbouvrette Apr 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in a295a33 — the exporter now throws an explicit error when a message has multiple variants that can't be represented in the .properties format.

To clarify: the .properties format can support plurals via ICU MessageFormat syntax within values (e.g., {count, plural, one {1 item} other {{count} items}}). In that model, plural forms live inside a single value string — every locale has the same set of keys, which avoids the key-mismatch problem that arises when plural forms are split across separate keys.

This constraint is not specific to .properties — any flat key-value format (including plain JSON) has the same limitation. The existing @inlang/plugin-json iterates all variants and writes them to the same key, meaning the last variant silently overwrites the others. We chose to throw an explicit error instead, which is the safer behavior — failing loudly rather than silently losing translation data.

This initial implementation treats values as opaque strings with {variable} interpolation. ICU MessageFormat parsing could be added as a follow-up. The error is a safety net for cases where the SDK hands the exporter multi-variant data from another plugin (e.g., plugin-icu1).

@nbouvrette
Copy link
Copy Markdown
Contributor Author

I have read the CLA Document and I hereby sign the CLA

github-actions Bot added a commit that referenced this pull request Apr 12, 2026
- Add changeset for version bump (minor)
- Copy properties-file logo SVG to plugin assets
- Add icon field to marketplace-manifest.json

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1. Escape control characters in exported values — backslashes, newlines,
   carriage returns, and tabs are now properly escaped when serializing
   to .properties format, preventing file corruption on roundtrip.

2. Throw explicit error on multi-variant messages — instead of silently
   dropping variants beyond the first, the exporter now throws a clear
   error explaining that .properties files do not support plural/select
   variants.

Added 2 tests covering both fixes (21 total).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Verify that ICU plural and select syntax in .properties values survives
roundtrip as plain text. The plugin treats ICU syntax as opaque string
content — it does not parse it into inlang's internal variant/selector
model. This is correct behavior: ICU parsing is the responsibility of a
dedicated ICU plugin, not the file format plugin.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@samuelstroschein
Copy link
Copy Markdown
Member

@nbouvrette thanks for the PR!

Are you willing to host the plugin in your own repo and I'll list it on inlang.com? I fear that i might need to maintain the Java properties plugin

@nbouvrette
Copy link
Copy Markdown
Contributor Author

@nbouvrette thanks for the PR!

Are you willing to host the plugin in your own repo and I'll list it on inlang.com? I fear that i might need to maintain the Java properties plugin

I didn't really think about this - I'm just starting to use inlang but as you probably noticed by the properties-file repo I maintain, I prefer this file type when doing localization - I think it would be a good built-in plugin to support and with AI tools the maintenance should be very low in my opinion. If you feel strongly about this I can create a separate repo for this but given I am just starting with inlang there might be a risk there as well for me to maintain this on the long run

@samuelstroschein
Copy link
Copy Markdown
Member

@nbouvrette i'd prefer a separate repo. could you do that?

@nbouvrette
Copy link
Copy Markdown
Contributor Author

@samuelstroschein turns out I put my localization project on hold for now. Should I close this PR? not sure when I will get back to it - I do think it would be useful for you to support .properties file on the other hand

@samuelstroschein
Copy link
Copy Markdown
Member

Closing for now then. Nothing against someone hosting the properties plugin themselves. I dont want to maintain it

@github-actions github-actions Bot locked and limited conversation to collaborators May 3, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants