JetBrains’ IDEs are available in English, Simplified Chinese, Japanese and Korean. Wouldn’t it be great if your plugin automatically adapted to this language?
Well, it’s possible – but there’s no “automatically” for you this time 😉
I recently added support for Simplified Chinese to my plugin BashSupport Pro. This wasn’t as straight-forward as you might expect. Actually, almost nothing worked out-of-the-box, and it was a frustrating experience.
By sharing what I learned I hope to make things a bit easier for others 😄
What are Language Packs?
The language of the user interface is controlled by the installed language pack.
To switch the language of your IDE’s user interface, install a language pack from the Marketplace. Only one language should be installed at a time, for example Simplified Chinese and Japanese shouldn’t be installed at the same time. Only one of the available language packs will be used to provide the localized resources to the IDE and plugins. The installation of a language pack requires a restart – switching the language without a restart is not possible.
2020.1 was the first version with support for a localized user interface. This article is about 2021.3 and later, because the language packs have evolved a bit. But, to a degree, it’s technically possible to support even older major versions.
Technically, a language pack is a plugin. It implements a single extension point and bundles localized resources for all products and plugins it supports.
For example, the Chinese language pack is doing this in its
So far, JetBrains is offering three different language packs:
Each of the packs supports multiple JetBrains products and even plugins.
It’s JetBrains products and plugins and that is the major pain point here – 3rd-party plugins can’t use the infrastructure created for language packs. You have to write some code to make things work in your plugin.
Contents of a Language Pack
As mentioned above, a single pack supports multiple JetBrains products and JetBrains plugins. That’s why the same language pack plugin is installed in IntelliJ IDEA, GoLand or PhpStorm.
A language pack contains the following resources:
- Message bundles, e.g. actions or tool window labels
- File templates
- Inspection descriptions
- Intention descriptions
- Postfix templates
- Searchable options
- Daily tips
Why Most Things Don’t Work for 3rd-party Plugins
The centerpiece of localization is
It’s widely used by the SDK to localize most user-visible texts.
The official SDK guide even explains how to localize your IDE. But, as far as I understand, this is only about the localization of the IDE itself and was most likely written before language packs were invented.
DynamicBundle is a
ResourceBundle implementation and most likely you’re already familiar with this concept.
Unfortunately, it always delegates the loading of localized resources to the language pack’s classloader.
The consequence is that it properly loads the English base bundle from your plugin’s resources. But as soon as a language pack is installed, the localized variant of the base bundle is searched in the contents of the language pack plugin. Of course, it’s not there – unless you’re working on a JetBrains plugin or product 😉
This is actually a clever concept – you can keep the same resource paths in your code and provide a localized copy with a language pack plugin. But unfortunately this only works for JetBrains’ products and plugins because adding 3rd-party content to a language pack doesn’t make sense, of course.
There’s a workaround – you can make your own bundle implementation.
Localizing Your Plugin
Create Your Own Message Bundle
First, you need to add your own base implementation of a message bundle.
It must use your plugin’s own classloader to lookup the localized variants of your
- Create the base implementation.
There’s already some code in the sample plugin repository: DynamicPluginBundle.
- Create the message bundle for your
Extend the base implementation, pass the path to your message file to the
superconstructor. Optionally, add a few helper methods. PluginBundle is similar to what I’m using in production code.
Here’s the code of
PluginBundle, it’s easy to read.
Personally, I prefer to have a
static helper method, because this allows to add
@PropertyKey(resourceBundle = "messages.pluginBundle") to the message key parameter – this gives you nice code completions from your
.properties file in IntelliJ IDEA.
The official SDK guide explains how to localize your actions: Localizing Actions and Groups.
You have to work a bit harder to make things work for own plugin.
Retrieve the localized text and description using code and pass it to the constructor of
Make sure to use the pattern for the property key as shown in the official guide:
action.<your action ID>.text and
action.<your action ID>.description.
As soon as JetBrains supports localization of non-JetBrains plugins out-of-the-box, you can drop the call to
Here’s some code, taken from our sample plugin:
There are a few elements you can’t translate, at least I wouldn’t know how to do this.
All of these use a
DynamicBundle, which breaks things for our plugin (again):
Inspections are similar.
As far as I understand the code, inspections configured in your
plugin.xml are wrapped by an
This wrapper is responsible to create an instance of your implementation and also manages the user-visible texts.
To localize the name and group name of an inspection, you have to override methods
getGroupDisplayName(): sample implementation.
Don’t forget to remove attributes
groupKey= from your plugin.xml.
What’s left now is the description, which is shown in the settings dialog, for example.
At first, I was excited that there seemed to be a simple way to provide it:
loadDescription() of the inspection implementation.
But unfortunately there’s this line.
Unless you manage to cause an
IOException, your implementation’s
loadDescription() is never called.
Instead, our lovely
DyanmicBundle is called, which only speaks English with 3rd-party plugins 😉
The only way I could find to provide a translated description is to override
This code demonstrates how to do this.
This should work well enough, but it’s a hack, and I hope that there will be an official solution in one of the next major versions.
Refer to the official SDK guide to learn how to configure your intentions.
It’s possible to localize the name of the intention by calling
setText() with a value retrieved from your message bundle.
getFamilyName() to provide a translated family name.
Unfortunately, I’m not aware of any way to provide a translated intention description.
The description is stored at
intentionDescriptions/<your intention directory name>/description.html.
If my understanding of the code is correct, then
ResourceTextDescriptor is responsible to load it and delegates to our beloved friend
DynamicBundle, which still stubbornly refuses to speak anything else than English.
Here’s a sample implementation of a localized intention.
As you might expect, localizing a configurable isn’t possible using the XML-based configuration.
Instead, you can override
getDisplayName() to provide a translated tree item in the settings dialog.
Here’s a sample implementation of a localized configurable.
By default, live template have a static description, which is stored in the XML snippet.
The intellij-community sources use a
key= attribute instead of
description= to localize the text (example).
I couldn’t find any reference to it in the official SDK guide, but it seems to be using DynamicBundle again.
I’m not aware of any other way to translate the description of your live templates.
Even the language packs don’t have translated display names of file templates, so I don’t think it’s possible (yet).
It seems to be possible to localize the HTML description of file templates, but again only with a language pack.
I’m feeling too tired now to dig into intellij-community to find if that’s actually the case.
But I’m pretty sure you don’t want me to mention again that
DyanmicBundle is NOT speaking Chinese 😉
Congratulations, you’ve made it and localized your plugin as much as possible!
But there’s still more to do! Unless you’re speaking Chinese, Korean or Japanese you still need to find a translator…
So far, I only have experience with the localization into Simplified Chinese.
For BashSupport Pro, which contains many references to technical terms, I wanted a native speaker of Chinese
who actually understands what shell scripts are and do.
The natural choice would be a developer who’s also a professional translator for Simplified Chinese.
I couldn’t find such a unique person, but found a kind Chinese developer instead who was willing to translate the plugin.
If you’re developing a paid plugin, you most likely have a website – at least you should have one 😉
I suppose that developers, who prefer the Chinese language pack in the IDE, would also like a Chinese website instead of English. For BashSupport Pro, there’s a translation of the website.
Even though it’s difficult to measure the impact of a localized plugin and website, I consider it professional to integrate with the IDE as much as possible.
I used Upwork and the JetBrains’ plugin developer Slack to find suitable translators for website and plugin. BashSupport Pro has about 800 different messages, including ShellCheck messages. Altogether, the total cost for both translation was in the range of 1000 to 2000 USD.
Should I use
Please don’t. This method changes the defaults of the IDE’s JVM process and has side effects for all other plugins.
The IDE doesn’t use it to choose the language of the user interface – that’s what language packs are for.
It’s better to align with the IDE and use the language pack’s locale instead, e.g. by calling
Why Can’t I Just Add My Own Language Pack?
Because you’d make a mess 😉 As explained above, only a single pack may be installed at a time. Adding your own would override the existing language pack and then only your plugin would be localized but nothing else.
Limitations of the SDK
This is a collection of shortcomings I found.
Ideally, the SDK’s
DynamicBundle should first try to fetch the translated resources from the plugin’s classloader before delegating to the language pack classloader.
That should solve most issues, but I’m not entirely sure that this would work as I imagine.
List of limitations of 2021.3 for 3rd-party plugins:
- Action texts and description are not translated (workaround above)
- Action overrides are not translated (no known workaround)
- Action synonyms are not translated (no known workaround)
- Inspection display name and group display name are not translated (workaround above)
- Inspection descriptions are not translated (workaround above)
- Intention descriptions are not translated (no known workaround)
- Intention before/action contents are not translated (no known workaround)
- Configurable names are not translated (workaround above)
- Notification group labels (no known workaround when using xml-based configuration)
- Live template names and descriptions are not translated (no known workaround, details above)
- File template descriptions are not translated (no known workaround)
There’s certainly more, but so far that’s all I had to research for my own work.
You can find an example of a localized plugin at github.com/jansorg/intellij-plugin-localization.
Here’s a neat trick in the build definition to automatically install the Chinese language pack when you execute