Integration translations
Every section of an integration's strings.json, how references and placeholders work, and how custom integrations ship their own translations.
Every piece of text your integration shows to users lives in strings.json, in the folder of your integration. Code never contains user facing text: it refers to a key, and Home Assistant looks up the text in the language of the user. That covers the config flow, entity names and states, errors, repair issues, actions, and more.
strings.json holds the English source text. After your pull request is merged, the English strings are uploaded to Lokalise, translators translate them, and the translations are pulled back into core. You do not need to do anything for that. See how translation works for the whole journey, and make your strings translatable for how to write strings that translators can work with.
This page is the reference for the file itself: which sections exist and what goes in them. The structure is checked by hassfest (script/hassfest/translations.py in core), so when this page and hassfest disagree, hassfest wins.
Home Assistant does not read strings.json directly. It reads the compiled files in the translations folder of the integration, which are generated and not committed. Compile your strings to see them in the UI:
python3 -m script.translations develop --integration exampleLeave out --integration to be asked for one, or use --all for all integrations. If a change does not show, restart Home Assistant and clear the cache of your browser.
All sections are optional. Add the ones your integration needs.
| Section | What it translates |
|---|---|
title |
The name of the integration, only when it is not a brand name |
common |
Strings your integration reuses in several places |
config |
The config flow |
options |
The options flow |
config_subentries |
The flows of subentries |
selector |
Options and labels of selectors in flows, actions, triggers, and conditions |
entity |
Names, states, state attributes, and units of your entities |
entity_component |
States and attributes of an entity domain, for integrations that are one, like sensor |
device |
Names of your devices |
exceptions |
Messages of exceptions you raise |
issues |
Repair issues and their repair flows |
services |
Names and descriptions of your actions and their fields |
triggers |
Names and descriptions of your triggers |
conditions |
Names and descriptions of your conditions |
device_automation |
Device triggers, conditions, and actions |
system_health |
Labels on the system health page |
application_credentials |
The explanation in the application credentials dialog |
preview_features |
Labs preview features |
A few more sections exist for specific integrations: config_panel for integrations that bring their own configuration panel, like ZHA and KNX, and conversation for the conversation integration. The auth, onboarding, and homeassistant_hardware integrations each have their own schema.
The name of your integration comes from the manifest, and is a brand name in almost every case. Brand names are not translated, so leave title out.
Only add title when the name is a description rather than a brand, like "Local calendar". For core integrations, hassfest refuses a title that equals the name in the manifest, unless the integration is on its allow list.
config, options, and each type under config_subentries share one format:
{
"config": {
"flow_title": "{name} ({host})",
"step": {
"user": {
"title": "Connect to your hub",
"description": "Enter the address of your Example hub.",
"data": {
"host": "[%key:common::config_flow::data::host%]"
},
"data_description": {
"host": "The hostname or IP address of your hub."
}
}
},
"error": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]"
},
"abort": {
"no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]"
}
}
}| Key | What it is |
|---|---|
flow_title |
The title of a flow in the list of discovered devices. See the flow title. |
step |
Per step ID: title, description, data, data_description, sections, menu_options, menu_option_descriptions, and submit (the label of the submit button) |
error |
Errors shown on a form, keyed by the error you return |
abort |
Reasons a flow aborts. Supports Markdown. |
progress |
Messages for a progress step, keyed by progress_action |
create_entry |
Messages shown when the entry is created. default is used when you pass no description. |
initiate_flow |
Labels of the buttons that start a flow. user is required when you add it. |
Every key in data_description needs a matching key in data. The keys of a step are explained in forms, menus, and progress.
Subentry types also need entry_type and initiate_flow. See subentries. Helper integrations must give their user step a description.
Selectors in a flow, an action, a trigger, or a condition get their own text through a translation_key in their config. The text goes under selector, keyed by that translation key:
{
"selector": {
"fan_speed": {
"options": {
"off": "[%key:common::state::off%]",
"low": "Low",
"high": "High"
}
}
}
}SelectSelector(
SelectSelectorConfig(
options=["off", "low", "high"],
translation_key="fan_speed",
)
)Which keys apply depends on the selector:
options: the options of a select selectorunit_of_measurement: the units of a number selectorchoices: the choices of a choose selectorfields: the fields of an object selector, each with aname, an optionaldescription, and an optionaloverview_label
An object selector shows the items you added as a list, with the values of their fields. The overview_label of a field sets the text for its value in that list. It must contain the {value} placeholder and no other placeholders. The frontend fills in the formatted value of the field, so your code passes nothing. Without an overview_label, the list shows only the value.
{
"selector": {
"sensor_offsets": {
"fields": {
"uncompensated": {
"name": "Uncompensated value",
"description": "The value before the offset is applied.",
"overview_label": "Uncompensated value: {value}"
}
}
}
}
}The entity section translates your entities, keyed by entity domain and then by the translation key of the entity:
{
"entity": {
"sensor": {
"operating_mode": {
"name": "Operating mode",
"state": {
"eco": "Eco",
"boost": "Boost"
},
"state_attributes": {
"fan_mode": {
"name": "Fan mode",
"state": {
"quiet": "Quiet"
}
}
}
},
"goal": {
"name": "Goal",
"unit_of_measurement": "steps"
}
}
}
}name: the name of the entity. See translated names, including placeholders in names.state: the text for each state. States are lowercase keys, likeeco, never text. See the select entity for an example.state_attributes: the name and the states of attributes.unit_of_measurement: a unit that is a word, like "steps". An entity with a translated unit does not setnative_unit_of_measurement. See sensor.
States, state attributes, and units cannot contain placeholders. Icons use the same translation key, in icons.json. See icons.
Entity translations are a rule of the Gold tier of the quality scale: see entity translations.
Integrations that are an entity domain, like sensor or binary_sensor, translate their states and attributes under entity_component, keyed by device class. The key _ is for entities without a device class:
{
"entity_component": {
"_": {
"name": "Binary sensor",
"state": {
"off": "[%key:common::state::off%]",
"on": "[%key:common::state::on%]"
}
},
"door": {
"name": "Door",
"state": {
"off": "[%key:common::state::closed%]",
"on": "[%key:common::state::open%]"
}
}
}
}Each device class can have a name, state, and state_attributes, in the same shape as under entity. Other integrations reuse these strings with references, like [%key:component::sensor::entity_component::temperature::name%].
The device section translates device names, keyed by the translation key you pass in the device info:
{
"device": {
"power_strip": {
"name": "Power strip with {socket_count} sockets"
}
}
}With a translation key, the name of the device info is ignored. If the key has no translation, the translation key itself becomes the name. Fill placeholders with translation_placeholders. See the device registry.
Exceptions that extend HomeAssistantError take a translation_domain, translation_key, and translation_placeholders. Their messages go under exceptions:
{
"exceptions": {
"cannot_connect": {
"message": "Could not connect to {host}."
}
}
}See setup failures and exceptions for where to raise them, and the exception translations rule of the quality scale.
Repair issues go under issues, keyed by the translation key of the issue. Each issue has a title, an optional short_title, and either a description or a fix_flow, never both. A fix_flow has the same format as a flow. See repairs.
The names and descriptions of your actions go under services:
{
"services": {
"set_speed": {
"name": "Set speed",
"description": "Sets the speed of the fan.",
"fields": {
"speed": {
"name": "Speed",
"description": "The speed to set."
}
},
"sections": {
"advanced": {
"name": "Advanced options"
}
}
}
}
}Every action needs a name and a description, and every field needs a name. Fields can also have an example. Fields inside a collapsible section still go under fields; sections only names the sections. See actions.
triggers and conditions work like services: per trigger or condition, a name, a description, and fields with a name, an optional description, and an optional example. They have no sections. See triggers and conditions for how to name them.
Device triggers, conditions, and actions go under device_automation, in trigger_type, trigger_subtype, condition_type, and action_type. Extra fields of a device automation get labels in extra_fields and descriptions in extra_fields_descriptions. See device automations.
system_health: the labels of your system health information, underinfo. See system health.application_credentials: adescriptionfor the dialog where users enter their credentials. See OAuth 2.0.preview_features: anameanddescriptionper feature, plus optionalenable_confirmationanddisable_confirmation. The description can contain links, but no placeholders. See Labs.
Many strings already exist, like "Failed to connect" or "Device is already configured". Do not write them again: point to them with a reference, written as [%key:<path>%]. Translators then translate the string once, for every integration that uses it.
The path uses :: between the parts:
common::...points to the shared strings inhomeassistant/strings.jsonin core, like[%key:common::config_flow::data::host%].component::<domain>::...points to any integration, including your own, like[%key:component::example::config::step::user::title%].
For strings you use several times, put them in your own common section and refer to them with [%key:component::example::common::<key>%].
A reference must be the whole string: "Error: [%key:common::...%]" is refused. A reference also cannot point to a string that is itself a reference, because Lokalise only supports one level. Point to the original instead. The compile step replaces the references with the text, so the files Home Assistant reads contain only plain strings.
Values that are only known at runtime, like a host name or a count, go in placeholders: "Could not connect to {host}.". Your code fills them in:
| Where | How you pass them |
|---|---|
| Flow steps | description_placeholders on async_show_form, async_show_menu, async_abort, and async_create_entry |
| Flow title | title_placeholders in self.context |
| Entities and devices | translation_placeholders |
| Exceptions and repair issues | translation_placeholders |
| Actions | description_placeholders when you register the action |
Hassfest checks every string:
- Placeholder names are valid Python identifiers, like
{host_name}. - Placeholders are not wrapped in single quotes, like
'{name}'. - Strings contain no HTML, and no spaces at the start or end.
- Strings contain no URLs. Put a URL in a placeholder instead, so a changed link does not need new translations.
- Translation keys are lowercase letters, digits,
-, and_. They do not start or end with-or_, and have no two of them in a row.
At runtime, a translation that misses a key falls back to English. A translation whose placeholders differ from the English string is dropped, and the English string is used instead.
Custom integrations do not use strings.json, and they are not translated on Lokalise. The compile step that turns strings.json into translations only runs in core. A custom integration ships the finished files itself, in its translations folder:
custom_components/example/
├── __init__.py
├── manifest.json
└── translations/
├── en.json
└── nl.jsonEach file is named after a BCP 47 language code and has the same structure as strings.json. en.json is required and complete, because it is the fallback for every other language.
References do not work in these files: nothing replaces them, so users see the raw [%key:...%] text. Write out every string in full. Placeholders work as usual.
Hassfest validates translations/en.json of a custom integration too. You can run it in your own repository with the hassfest GitHub Action.
Questions about strings.json or a hassfest error? Ask in the #core channel on the Home Assistant Discord. For questions about translating itself, see the Translate area.