Zorann
Zorann

Reputation: 397

Godot translation - how to deal with longer strings in other languages?

The question as above, after translation server does its work, some languages have much longer strings than others. I'm aware that containers might resize, or that I can clip text, or wrap it. But what if I have set containers size and do not want to change it, and wrappping text does not help? My idea is to resize text, but I don't know what should I do after TranslationServer.set_locale(lang) in order to do so?

My best bet was, to set all labels/buttons clip_text properity to true and do such thing:

TranslationServer.set_locale(lang)
loop through entire tree:
    if CURRENT_NODE does have properity .text:
        var clipping = true
        while clipping
             if CURRENT_NODE clip_text properity is currently NOT clipping anything #I have no idea how to check this?
                 clipping = false
             else:
                 CURRENT_NODE font_size -= 1

My next idea was to turn clip_text off and loop through every node like above, but instead of checking clipping activity, making an invisible for user copy of the node that does copy node width size as well, but this time allow the node to resize, and measure the node width, compare to original, and resize font like above until the copied node will be the same or lower width, and then set established int his way fint size to original node. But this is crazy overkill, and seems like a weird thing to do, right? It need to be done every time for every new node that contains text, and if any text does change over and over again. Is this how it suppose to look like?

Oversized strings after translations seems like a common problem, right? But I'm unable to find any reasonable solutions or examples that might help me in my case. Any help would be great.

EDIT:

@Theraot Thank You for such great answer! The main idea that You are suggesting is, that I should design game for the longest string possible and work arund it. I do agree, however:

  1. Pseudolocalization would be helpful, if it shows always the longest string that occurs in the languages pool, so I can react accordingly, but it does no such thing, therefore it is not very helpful in my eyes. I still need to cycle through all languages and check every one separately - this is not handy. Are there no tools that can make the process less painful?
  2. There is a case where I want to resize text anyway: If I plan to lets say use font size 20 and it does looks good, and it comes up, that german is the only one that fits only with font size 16, it would be bad design to scale down all langauges to look worse only to handle one language that might look a little worse. Yet I don't want to set font size manually for each language obvious reasons - there are too many labels and different languages combinations. If I make sure that font size does not become extreamly long tha tit would lead to font being too small, than I can leave the font resizing for the script. In my eyes this does justify the resizing strategy.
  3. Here is another problem - I simply can't check by script (in a simple, reasonable for the task way), how to measure if text is too big, so I can't apply my loop that makes font smaller. As above in my pseudocode - one solution would be to check if clipping text is actually clipping label, but I'm unable to check this properity behaviour state. I came up with new solution: use text_overrun_behaviour and set it to Trim Characters, and then check how many characters are actually displayed. This would be perfect way to achieve such simple thing, but I can't find any way to check the label text actual size, and I'm really surprised, that this is not a simple task.
  4. Another thing that bothers me: When I'm getting label text by $label.text it does return the placeholder for translation server, and not the actual text that is inside the label. I understand why this is the case, however in order to get current text I need to use tr($label.text) - and this is also "fine", but I obviously do not get actual displayed (and trimed, or simply changed via dynamic %s) text. Am I missing some obvious solution on how to get the label text that user does see, and not the placeholder?
  5. Last thing: when I use pseudolocalization, the game does look weird, as intended with pseudolocalization, but if I want to print something, what would be a single line, I get such output that makes using pseudocode even more useless for debuging process: Unicode parsing error, some characters were replaced with � (U+FFFD): NUL character => A LOT of this, and finally: [output overflow, print less text!]. It does happen even if I tested it with print("test"). What is going on here? I am using font that does have normal alphabet and we are talking about languages that uses "normal" characters.

Upvotes: 2

Views: 424

Answers (1)

Theraot
Theraot

Reputation: 40315

First advice is to design and test with long text. Which you can approach with Pseudolocalization.

You seem to want to scale down (making the text smaller, be aware of stretch mode in project settings), which might lead to the font being ilegible.


Allowing UI to scale

With that out of the way...

I'd suggest to set autowrap_mode to AUTOWRAP_OFF or AUTOWRAP_WORD_SMART in Label and RichTextLabel.

And set fit_content to true in RichTextLabel. So they resize with the text.

If use visible_characters, set visible_characters_behavior to VC_CHARS_AFTER_SHAPING.


Yes, this brings issues with aligment (e.g the Control resizes and it is no longer centered), you can react to the resize (or both theme and locale changes) and use set_anchors_and_offsets_preset.

I have this in my labels:

func _notification(what: int) -> void:
    if what == NOTIFICATION_TRANSLATION_CHANGED or what == NOTIFICATION_THEME_CHANGED:
        _update_text()

Where _update_text() does whathever I need to do. I use a deferred call to execute set_anchors_and_offsets_preset.

You can use a similar approach for OptionButton.


OK, that is labels, but other controls might need to resize, or do not accomodate the customization you want.

For example, if the default behavior of Button is not good, you can create a custom Button class that instantiates a Label inside. The Label works as described above. And the Button resizes itself to the size of the Label (plus some extra margin if you want). You can do this in _process or in the resized signal of the Label

The same idea can work for a Container that resizes to match the size of the children.

A more general solution would be to have a Control that copies the size and position of another Control, and add an extra margin. I can give the code in full for that:

@tool
extends Control


@export var source:Control:
    set(mod_value):
        if source == mod_value:
            return

        source = mod_value
        if is_node_ready():
            _update_source()


@export var grow_left:float
@export var grow_top:float
@export var grow_right:float
@export var grow_bottom:float


func _ready() -> void:
    _update_source()


func _exit_tree() -> void:
    if is_queued_for_deletion():
        return

    request_ready()


func _process(_delta: float) -> void:
    if not is_instance_valid(source):
        _update_source()
        return

    var parent_control := get_parent_control()
    # global transform of the parent
    var parent_control_global_transform := Transform2D.IDENTITY
    if is_instance_valid(parent_control):
        parent_control_global_transform = parent_control.get_global_transform()

    # global transform of source
    var _source_global_transform := source.get_global_transform()
    # transform of source relative to the parent
    var _source_to_local_transform := parent_control_global_transform.affine_inverse() * _source_global_transform

    visible = source.visible
    size_flags_horizontal = source.size_flags_horizontal
    size_flags_vertical = source.size_flags_vertical
    size_flags_stretch_ratio = source.size_flags_stretch_ratio
    custom_minimum_size = source.get_combined_minimum_size() + Vector2(grow_left + grow_right, grow_top + grow_bottom)
    size = source.size + Vector2(grow_left + grow_right, grow_top + grow_bottom)
    global_position = _source_global_transform.origin - Vector2(grow_left, grow_top)
    rotation = _source_to_local_transform.get_rotation()
    scale = _source_to_local_transform.get_scale()


func _update_source() -> void:
    if not is_inside_tree():
        return

    if not is_instance_valid(source):
        source = get_parent_control()

    set_process(is_instance_valid(source))

You can think of this as a poor man's remote transform for Control.

The above code uses _process because not all changes have appropiate notifications.

So you can, for example, have it follow the size and position of a VBoxContainer that has Buttons that might resize due to local changes... And then inside of it have Panel with an StyleBox that provides a border around the VBoxContainer.


In some cases you might also resource to using HFlowContainer and VFlowContainer, so your Controls can move to another row or column.

And I believe the last cases would be when you want to hide the overflow and provide a "see more" option or pagination, which I believe would be made bespoke case by case.


Addendum: Skewmorphic UI

After writing the answer the idea that perhaps you didn't want to change the size of the elements because they are skewmorphic.

For exmaple, we might be talking about a playing card with some text on it, it makes not sense to change the size of the card to fit the text.

However, it remains true that making the text smaller can lead to the text not being legible.

I can think of a few compromises:

  • Add scroll bars.
  • Use a short translation, and add a tooltip for the full translation.
  • Have a secondary UI to visualize the text, that way it does not matter if it is not legible in the skewmorphic elements.

I would expect that skewmorphic are not everywhere on your design, and so you can have dedicated code to scale them, and so you do not have to iterate over every node as you say in the question.


Addendum on updated question

On numberal 1: Yes you need to test all your languages. I don't know of a tool to make it easier, but even with a tool, testing is important.

On numeral 2 and 3: What comes to mind is to use a hidden Label to measure the size without wrapping and compare it. There might be other ways to do it, such as drawing the text to a bitmap and measuring it there, but that seems too much trouble. Perhaps something on TextServer is useful, but I'm not sure. Addendum from comments: This was solved with the get_string_size method from the Font class.

On numeral 4: I disable autotranslation, and instead have code that sets the translated text when I get NOTIFICATION_TRANSLATION_CHANGED. This also allows me to process the translated text before assigning it (e.g. string format or regex replace).

On numeral 5: Idk why you would get unicode errors, that might be a bug. If you can make a minimun reproduction project, please report it at https://github.com/godotengine/godot/issues - About the overflow, you might be able to increase the output buffer size. It is in Project Settings -> Network -> Limits -> Debugger.

Upvotes: 2

Related Questions