godotengine / godotengine/godot-docs

Add an editor development page about handling localization

Open
#12,373 0 comments 1 reaction 0 assignees View on GitHub
Dominant language
reStructuredText
Stars
5.7k
Forks
3.8k
Avg merge
1d 20h
Merged PRs (30d)
25

Description

**Your Godot version:**

4.8

**Issue description:**

Since https://github.com/godotengine/godot/issues/104574 exists, I think we need a doc page about how to properly handle editor localization. The process isn't currently properly formalized. I'm not really good at writing docs, but I have some notes about it that could be transformed into something readable.

---

The editor's localization effort has 2 goals in general: all relevant strings can be translated, and the editor language can be switched without restarting the editor. There are various macros/methods to achieve these goals. The editor translations are extracted using RegEX-based scripts ([link](https://github.com/godotengine/godot-editor-l10n/tree/main/scripts)) and every translateable string should be marked in some way.

Note that, since the extraction scripts are running on raw source code, they can parse strings anywhere, e.g. inside comments. This can be sometimes abused for strings that don't exist in the code directly (e.g. they are capitalized from a raw name). They also don't support concatenated strings. Things like `TTR("Hello " + "world")` won't be properly extracted, it needs to be split into `TTR("Hello ") + TTR(world")`. Unquoted strings, like `TTR(variable)` are not extracted, but using translation functions may still be relevant and is allowed.

Relevant strings include pretty much anything that's displayed in the GUI. String that are shown only in the console/output should not be translated (this mostly applies to error messages).

### Static strings

For the most part, you should be using the TTRC macro. It's a no-op macro that just marks a string for extraction. It only supports static string literals, e.g. `TTRC("Update")`. The string is extracted by the extraction script, then once translations are synced and filled in the Weblate, the auto-translation system will automatically show this string as localized when a specific language is selected. There are however various cases where TTRC does not apply.

When you should use TTRC:
- The string is static, i.e. it's a singular string literal, not part of a bigger compound string (like strings that use `vformat()` or concatenated with `+`).
- The displayed text has enabled auto-translation (this can be controlled by `auto_translate_mode` property of Node, but also granuluar auto-translation in various Control nodes, like ItemList's item auto-translation etc.)
- The string is _not_ a property name or a documentation string.

Note that there are cases when TTRC can be used in the first 2 cases. The main purpose of TTRC is to mark a string as a translateable, without modifying it. The raw string can be later translated with TTR. For example, this is acceptable:
```C++
String string1 = TTRC("Hello")
String string2 = TTRC("world")
label.text = TTR(string1) + " " + TTR(string2)
```
Such constructs are rarely needed though, and they have to be refreshed manually like dynamic strings.

### Dynamic strings

For all kinds of dynamic strings you can use the TTR function. It will translate the given string in place. The caveat is that the string won't be subject to auto-translation and has to be manually refreshed on NOTIFICATION_TRANSLATION_CHANGED.

When you should use TTR:
- The string is constructed dynamically, e.g. `vformat(TTR("%d pieces"), n_of_pieces)` or `TTR("Hello") + "\n" + TTR("world")`
- The displayed text is not auto-translated (that includes Nodes with `auto_translate_mode` disabled, or strings drawn with `draw_string()`)

Also these things should always use TTR, even if TTRC could technically apply:
- Undo/redo action names
- Editor toaster messages

The reason is that these strings can be both static and dynamic, but they are displayed by the same GUI element regardless of their form, so the dynamic is assumed by default.

Dynamic strings have to be refreshed manually. That means, whenever `NOTIFICATION_TRANSLATION_CHANGED` is received, the text has to be updated to the new language. This is usually performed by re-assigning all affected strings. Since the notification is received automatically when the node enters tree, the string should not be assigned in the constructor like you would do normally, to avoid duplication.

This is wrong:
```C++
MyNode::_notification(int p_what) {
case NOTIFICATION_TRANSLATION_CHANGED:
button->set_tooltip_text(TTR("Press to do stuff.") + "\n" + TTR("Shift+Press to do other stuff."));
}

MyNode::MyNode() {
button = memnew(Button);
button->set_tooltip_text(TTR("Press to do stuff.") + "\n" + TTR("Shift+Press to do other stuff.));
}
```
The tooltip should not be assigned in the constructor in this case (it's redundant), only in the notification.

There are also cases where updating translation involves a more complex update that should not happen immediately. In that case you can guard the update with `is_ready()`:
```C++
MyNode::_notification(int p_what) {
case NOTIFICATION_TRANSLATION_CHANGED:
if (is_ready()) {
_update_tree();
}
}
```

The exceptions from auto-refresh are transient messages. For example error dialogs are exclusive, which means that you can't change the editor language while their text is being displayed, so it usually does not matter whether the string is static or dynamic. Do note however that it only applies to dialogs that set their text right before showing (most notably the EditorNode's `show_warning()` method).

### Plurals

When a string includes a counter, consisder using plural messages with TTRN. For example: `TTRN("%d step", "%d steps", step_count)`. Such strings are obviously dynamic and need to be manually refreshed.

For messages without a counter, e.g. "Node moved."/"Nodes moved.", you should _not_ use the TTRN. Either use a ternary operator, like
```C++
text = nodes.size() == 1 ? TTRC("Node moved.") : TTRC("Nodes moved."); // Note that TTRC/TTR distinction applies normally, depending on context.
```
or make the message "universal", like `TTRC("Node(s) moved.")`. The former is preferred, because universal messages are not viable for all languages.

### Translation-friendly strings

Ideally all translateable strings should only include the part relevant for translation, with the full context. As such:
- `vformat(TTR("%s somethings"), sth_counter)` is preferred over `itos(sth_counter) + TTR(" somethings")`
- `TTR("First line.") + "\n" + TTR("Second line.")` is preferred over `TTRC("First line.\nSecond line.")` (especially when separate lines are reused in other strings)
- Trailing whitespace should be removed, so `TTR("Node list:") + " "` is preferred over `TTRC(Node list: ")`

Note that in some cases a more friendly string means the string has to be dynamic.

### Auto-translate mode

Most nodes that display text support auto-translation, and in many cases that auto-translation is granural. For example you can set auto-translate mode for a Button and for its tooltip separately. This can be useful to make some strings static, while some part of the node is dynamic or has disabled auto-translation. Typical cases include:
- A button has a text that should not be translated, but its tooltip should be translated.
- A Tree/ItemList displays some items that should be translated, and some that shouldn't.
- For this case, the node's own auto-translate should apply to majority of the items, while other items should be set individually.

You should set auto-translate mode to disabled in these cases:
- Text is a class name. E.g. "Node2D".
- Does not apply to texts that _include_ a class name, e.g. "This is a Node2D."
- The text displays custom strings, like group names, node names etc.
- The text is a proper name. E.g. "Godot"

Also ideally, if a displayed string is dynamic, its auto-translate mode should be disabled. Otherwise the engine will translate the string twice (first for the dynamic string, second for auto-translation, which will obviously fail). However this is harmless, so it's not strictly followed.

### Runtime strings

Some editor strings are included in runtime code. Most notably this applies to configuration warnings. For such cases, there are RTR and RTRN functions, which are runtime equivalents of TTR/TTRN.

The runtime equivalent for TTRC is ETR, however it should only be used in GUI-displayed text of the node itself, and especially text that can display outside the editor context (i.e. at runtime). For example FileDialog labels use it. ETR strings are extracted into separate translation domain, which can be included in Template Generation for translations. There is no dynamic equivalent for ETR, you should use `atr(ETR())` for that.

### Properties

Most property names are automatically extracted from registration methods of ClassDB. There are however some dynamic properties that are not registered; for them you should use `PNAME`. This includes:
- Properties added with `_get_property_list()`, e.g.
```C++
p_list->push_back(PropertyInfo(Variant::INT, vformat("%s/%d/%s", PNAME("args"), i, PNAME("type"))));
```
- Properties registered with PropertyListHelper:
```C++
base_property_helper.register_property(PropertyInfo(Variant::STRING, PNAME("name"), PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NO_EDITOR), defaults.name, &MeshLibrary::set_item_name, &MeshLibrary::get_item_name);
```
> [!NOTE]
> This may change, based on how https://github.com/godotengine/godot/pull/123364 is handled.

- Strings that display in the inspector as a section, but the string is part of a hint, e.g.
```C++
ADD_PROPERTY(PropertyInfo(Variant::INT, "input_port_count", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_ARRAY, String(PNAME("Input Ports")) + ",input_port_,swap_method=move_input_port,add_button_text=" + String(TTRC("Add Port"))), "set_input_port_count", "get_input_port_count");
```
Note that, while PNAME is like TTRC, they are not interchangeable. The PNAME strings are extracted into a separate translation domain, which is used when localized properties/settings are enabled.

There is also GNAME, which can be used to extract dynamic property groups, like
```C++
p_list->push_back(PropertyInfo(Variant::NIL, GNAME("Terrains", ""), PROPERTY_HINT_NONE, "", PROPERTY_USAGE_GROUP));
```

### Doc strings

Docs also have a separate translation domain. Doc strings are marked/translated with DTR and DTRN. This is only useful for e.g. tooltips that pull text from the documentation, or strings that should be extracted as part of documentation.

**URL to the documentation page (if already existing):**

Would probably go here:
https://docs.godotengine.org/en/latest/engine_details/editor/index.html

Contributor guide

No contributing guide indexed for this repository

Research direction

Start at the editor documentation index linked in the issue and review the supplied notes on TTRC, TTR, TTRN, runtime strings, properties, and doc strings. Organize them into a readable development page covering the localization rules and refresh behavior, then verify that the page is discoverable from the editor documentation section.

Written by the indexing model from the issue text.

Assessment

Tech stack
cpp
Domain
documentation, internationalization, localization
Issue type
Documentation
Difficulty
3/5
Estimated time
1-2 days
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
74/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.