feat(label): implement api to bind a translation tag to a label (#8948)

Co-authored-by: Liam Howatt <30486941+liamHowatt@users.noreply.github.com>
This commit is contained in:
André Costa
2025-09-26 13:07:28 +02:00
committed by GitHub
parent 48a1ea968a
commit 2f40b98040
14 changed files with 276 additions and 61 deletions
@@ -76,6 +76,8 @@ These return a translated string which can be used with widgets:
lv_dropdown_set_options(dd, lv_tr("color_list"));
.. _translation_fallbacks:
Fallbacks
---------
@@ -89,6 +91,8 @@ Dynamically Updating UI Text
When :cpp:expr:`lv_translation_set_language("language")` is called, LVGL sends ``LV_EVENT_TRANSLATION_LANGUAGE_CHANGED`` to every widget, allowing you to update text automatically.
The new language can be retrieved by either calling :cpp:expr:`lv_translation_get_language()` or by getting the event parameter in the event callback with :cpp:expr:`lv_event_get_param(e)`
Basic Example
-------------
@@ -99,6 +103,11 @@ Basic Example
lv_obj_t * label = lv_event_get_target_obj(e);
const char * tag = (const char *) lv_event_get_user_data(e);
lv_label_set_text(label, lv_tr(tag));
/* You can get the new language with `lv_event_get_param`*/
/* const char * language = (const char *) lv_event_get_param(e); */
/* or with `lv_translation_get_language` */
/* const char * language = lv_translation_get_language(); */
}
lv_obj_t * label = lv_label_create(lv_screen_active());
+7
View File
@@ -76,6 +76,13 @@ they are stored in ROM memory, which is always accessible.
realloc() will be forced every time the length of the string changes. That
MCU overhead can be avoided by doing the above.
Set translation tag
-------------------
When using LVGL's translation module, you can bind a translation tag to a label directly with :cpp:expr:`lv_label_set_translation_tag(label, tag)`.
After this function is called, future changes to the language will automatically update the label's text to display the corresponding translation
for that tag in the new language.
.. _lv_label_newline:
Newline
+1 -17
View File
@@ -34,9 +34,7 @@ e.g., ``languages="en de hu"``. Language codes are free-form, but ISO-style code
Each ``<translation>`` defines a ``tag``, which acts as the lookup key, and attributes for each language.
Translations may be omitted --- fallbacks will be applied when needed.
TODO: link to more information about translation fallbacks: xml_translations_fallbacks
Translations may be omitted --- fallbacks will be applied when needed. See :ref:`translation_fallbacks` for more details.
To register XML translations:
@@ -44,17 +42,3 @@ To register XML translations:
- :cpp:expr:`lv_xml_translation_register_from_data(xml_string)`
Multiple XML sources can be registered; they will be merged and searched collectively.
Usage in XML
************
Some widget properties support a ``*-translated`` suffix to refer to translation tags. For example:
.. code-block:: xml
<lv_label text-translated="dog"/>
This sets the label's text to the translated string for ``"dog"``.
+1 -1
View File
@@ -25,7 +25,7 @@
</styles>
<view extends="lv_obj" style_radius="3" width="#size" height="content" styles="gray" style_bg_color="$bg_color" >
<lv_label text-translated="$title" align="left_mid"/>
<lv_label translation_tag="$title" align="left_mid"/>
<my_button btn_text="$action" align="right_mid">
<style name="$btn_rel_style"/>
<style name="$btn_pr_style" selector="pressed"/>
+7
View File
@@ -34,3 +34,10 @@ Monospace font
.. lv_example:: widgets/label/lv_example_label_6
:language: c
Assign a translation tag to a label
-----------------------------------
.. lv_example:: widgets/label/lv_example_label_7
:language: c
@@ -0,0 +1,59 @@
#include "../../lv_examples.h"
#if LV_USE_TRANSLATION && LV_USE_DROPDOWN && LV_USE_LABEL && LV_BUILD_EXAMPLES
static const char * tags[] = {"tiger", "lion", "rabbit", "elephant", NULL};
static const char * languages[] = {"English", "Deutsch", "Español", NULL};
static void add_static_translations(void)
{
static const char * translations[] = {
"The Tiger", "Der Tiger", "El Tigre",
"The Lion", "Der Löwe", "El León",
"The Rabbit", "Das Kaninchen", "El Conejo",
"The Elephant", "Der Elefant", "El Elefante",
};
lv_translation_add_static(languages, tags, translations);
}
static void language_change_cb(lv_event_t * e)
{
static char selected_lang[20];
lv_obj_t * dropdown = lv_event_get_target_obj(e);
lv_dropdown_get_selected_str(dropdown, selected_lang, sizeof(selected_lang));
lv_translation_set_language(selected_lang);
}
/**
* Use a translation tag in labels
*/
void lv_example_label_7(void)
{
lv_obj_set_flex_flow(lv_screen_active(), LV_FLEX_FLOW_COLUMN);
lv_obj_set_flex_align(lv_screen_active(), LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
add_static_translations();
const size_t tag_count = sizeof(tags) / sizeof(tags[0]) - 1;
const size_t lang_count = sizeof(languages) / sizeof(languages[0]) - 1;
/* Create a dropdown to be able to select the language */
lv_obj_t * language_dropdown = lv_dropdown_create(lv_screen_active());
lv_dropdown_clear_options(language_dropdown);
for(size_t i = 0; i < lang_count; ++i) {
lv_dropdown_add_option(language_dropdown, languages[i], i);
}
lv_obj_add_event_cb(language_dropdown, language_change_cb, LV_EVENT_VALUE_CHANGED, NULL);
lv_translation_set_language("English");
/* Create a label for each tag */
for(size_t i = 0; i < tag_count; ++i) {
lv_obj_t * label = lv_label_create(lv_screen_active());
lv_label_set_translation_tag(label, tags[i]);
}
}
#endif /*LV_USE_TRANSLATION && LV_USE_DROPDOWN && LV_USE_LABEL && LV_BUILD_EXAMPLES*/
+1
View File
@@ -98,6 +98,7 @@ void lv_example_label_3(void);
void lv_example_label_4(void);
void lv_example_label_5(void);
void lv_example_label_6(void);
void lv_example_label_7(void);
void lv_example_led_1(void);
+1 -2
View File
@@ -58,9 +58,8 @@ void lv_xml_label_apply(lv_xml_parser_state_t * state, const char ** attrs)
if(lv_streq("text", name)) lv_label_set_text(item, value);
else if(lv_streq("long_mode", name)) lv_label_set_long_mode(item, long_mode_text_to_enum_value(value));
#if LV_USE_TRANSLATION
if(lv_streq("text-translated", name)) lv_label_set_text(item, lv_tr(value));
else if(lv_streq("translation_tag", name)) lv_label_set_translation_tag(item, value);
#endif
if(lv_streq("long_mode", name)) lv_label_set_long_mode(item, long_mode_text_to_enum_value(value));
else if(lv_streq("bind_text", name)) {
lv_subject_t * subject = lv_xml_get_subject(&state->scope, value);
if(subject == NULL) {
+97 -40
View File
@@ -25,6 +25,7 @@
#include "../../stdlib/lv_sprintf.h"
#include "../../stdlib/lv_string.h"
#include "../../others/observer/lv_observer_private.h"
#include "../../others/translation/lv_translation.h"
/*********************
* DEFINES
@@ -48,6 +49,8 @@ static void lv_label_destructor(const lv_obj_class_t * class_p, lv_obj_t * obj);
static void lv_label_event(const lv_obj_class_t * class_p, lv_event_t * e);
static void draw_main(lv_event_t * e);
static void set_text_internal(lv_obj_t * obj, const char * text);
static void remove_translation_tag(lv_obj_t * obj);
static void lv_label_refr_text(lv_obj_t * obj);
static void lv_label_revert_dots(lv_obj_t * label);
static void lv_label_set_dots(lv_obj_t * label, uint32_t dot_begin);
@@ -138,43 +141,8 @@ lv_obj_t * lv_label_create(lv_obj_t * parent)
void lv_label_set_text(lv_obj_t * obj, const char * text)
{
LV_ASSERT_OBJ(obj, MY_CLASS);
lv_label_t * label = (lv_label_t *)obj;
/*If text is NULL then just refresh with the current text*/
if(text == NULL) text = label->text;
lv_label_revert_dots(obj); /*In case text == label->text*/
const size_t text_len = get_text_length(text);
/*If set its own text then reallocate it (maybe its size changed)*/
if(label->text == text && label->static_txt == 0) {
label->text = lv_realloc(label->text, text_len);
LV_ASSERT_MALLOC(label->text);
if(label->text == NULL) return;
#if LV_USE_ARABIC_PERSIAN_CHARS
lv_text_ap_proc(label->text, label->text);
#endif
}
else {
/*Free the old text*/
if(label->text != NULL && label->static_txt == 0) {
lv_free(label->text);
label->text = NULL;
}
label->text = lv_malloc(text_len);
LV_ASSERT_MALLOC(label->text);
if(label->text == NULL) return;
copy_text_to_label(label, text);
/*Now the text is dynamically allocated*/
label->static_txt = 0;
}
lv_label_refr_text(obj);
remove_translation_tag(obj);
set_text_internal(obj, text);
}
void lv_label_set_text_fmt(lv_obj_t * obj, const char * fmt, ...)
@@ -190,6 +158,7 @@ void lv_label_set_text_vfmt(lv_obj_t * obj, const char * fmt, va_list args)
LV_ASSERT_OBJ(obj, MY_CLASS);
LV_ASSERT_NULL(fmt);
remove_translation_tag(obj);
lv_obj_invalidate(obj);
lv_label_t * label = (lv_label_t *)obj;
@@ -217,6 +186,7 @@ void lv_label_set_text_static(lv_obj_t * obj, const char * text)
LV_ASSERT_OBJ(obj, MY_CLASS);
lv_label_t * label = (lv_label_t *)obj;
remove_translation_tag(obj);
if(label->static_txt == 0 && label->text != NULL) {
lv_free(label->text);
label->text = NULL;
@@ -230,6 +200,28 @@ void lv_label_set_text_static(lv_obj_t * obj, const char * text)
lv_label_refr_text(obj);
}
#if LV_USE_TRANSLATION
void lv_label_set_translation_tag(lv_obj_t * obj, const char * tag)
{
LV_ASSERT_OBJ(obj, MY_CLASS);
lv_label_t * label = (lv_label_t *)obj;
if(!tag) {
return;
}
char * new_tag = lv_strdup(tag);
LV_ASSERT_MALLOC(new_tag);
if(!new_tag) {
LV_LOG_WARN("Failed to allocate memory for new tag");
return;
}
if(label->translation_tag) {
lv_free(label->translation_tag);
}
label->translation_tag = new_tag;
set_text_internal(obj, lv_tr(tag));
}
#endif /*LV_USE_TRANSLATION*/
void lv_label_set_long_mode(lv_obj_t * obj, lv_label_long_mode_t long_mode)
{
LV_ASSERT_OBJ(obj, MY_CLASS);
@@ -796,6 +788,10 @@ static void lv_label_destructor(const lv_obj_class_t * class_p, lv_obj_t * obj)
if(!label->static_txt) lv_free(label->text);
label->text = NULL;
#if LV_USE_TRANSLATION
if(label->translation_tag) lv_free(label->translation_tag);
label->translation_tag = NULL;
#endif /*LV_USE_TRANSLATION*/
}
static void lv_label_event(const lv_obj_class_t * class_p, lv_event_t * e)
@@ -861,7 +857,17 @@ static void lv_label_event(const lv_obj_class_t * class_p, lv_event_t * e)
}
else if(code == LV_EVENT_DRAW_MAIN) {
draw_main(e);
}
#if LV_USE_TRANSLATION
else if(code == LV_EVENT_TRANSLATION_LANGUAGE_CHANGED) {
lv_label_t * label = (lv_label_t *)obj;
if(label->translation_tag) {
const char * new_text = lv_tr(label->translation_tag);
set_text_internal(obj, new_text);
}
}
#endif
}
static void draw_main(lv_event_t * e)
@@ -978,6 +984,59 @@ static void draw_main(lv_event_t * e)
layer->_clip_area = clip_area_ori;
}
static void set_text_internal(lv_obj_t * obj, const char * text)
{
lv_label_t * label = (lv_label_t *)obj;
/*If text is NULL then just refresh with the current text*/
if(text == NULL) text = label->text;
lv_label_revert_dots(obj); /*In case text == label->text*/
const size_t text_len = get_text_length(text);
/*If set its own text then reallocate it (maybe its size changed)*/
if(label->text == text && label->static_txt == 0) {
label->text = lv_realloc(label->text, text_len);
LV_ASSERT_MALLOC(label->text);
if(label->text == NULL) return;
#if LV_USE_ARABIC_PERSIAN_CHARS
lv_text_ap_proc(label->text, label->text);
#endif
}
else {
/*Free the old text*/
if(label->text != NULL && label->static_txt == 0) {
lv_free(label->text);
label->text = NULL;
}
label->text = lv_malloc(text_len);
LV_ASSERT_MALLOC(label->text);
if(label->text == NULL) return;
copy_text_to_label(label, text);
/*Now the text is dynamically allocated*/
label->static_txt = 0;
}
lv_label_refr_text(obj);
}
static void remove_translation_tag(lv_obj_t * obj)
{
LV_UNUSED(obj);
#if LV_USE_TRANSLATION
lv_label_t * label = (lv_label_t *)obj;
/* Remove translation tag so we don't update the text automatically if the language changes*/
if(label->translation_tag) {
lv_free(label->translation_tag);
label->translation_tag = NULL;
}
#endif /*LV_USE_TRANSLATION*/
}
static void overwrite_anim_property(lv_anim_t * dest, const lv_anim_t * src, lv_label_long_mode_t mode)
{
switch(mode) {
@@ -1409,7 +1468,5 @@ static void label_text_observer_cb(lv_observer_t * observer, lv_subject_t * subj
}
}
#endif /*LV_USE_LABEL*/
#endif
#endif /*LV_USE_LABEL*/
+13
View File
@@ -158,6 +158,19 @@ void lv_label_set_text_selection_end(lv_obj_t * obj, uint32_t index);
*/
void lv_label_set_recolor(lv_obj_t * obj, bool en);
#if LV_USE_TRANSLATION
/**
* Assign a translation tag for this label. Memory will be allocated to store the tag by the label.
* The label text will automatically update when the language is changed via `lv_translation_set_language`.
* @param obj pointer to a label object
* @param tag '\0' terminated character string.
*/
void lv_label_set_translation_tag(lv_obj_t * obj, const char * tag);
#endif /*LV_USE_TRANSLATION*/
/*=====================
* Getter functions
*====================*/
+3
View File
@@ -31,6 +31,9 @@ extern "C" {
struct _lv_label_t {
lv_obj_t obj;
char * text;
#if LV_USE_TRANSLATION
char * translation_tag;
#endif /*LV_USE_TRANSLATION*/
char dot[LV_LABEL_DOT_NUM + 1]; /**< Bytes that have been replaced with dots */
uint32_t dot_begin; /**< Offset where bytes have been replaced with dots */
+60
View File
@@ -789,5 +789,65 @@ void test_label_wrap_mode_clip(void)
lv_snprintf(buf, sizeof(buf), "widgets/label_wrap_clip.png");
TEST_ASSERT_EQUAL_SCREENSHOT(buf);
}
void test_label_translation_tag(void)
{
static const char * tags[] = {"tiger", NULL};
static const char * languages[] = {"en", "de", "es", NULL};
static const char * translations[] = { "The Tiger", "Der Tiger", "El Tigre" };
lv_translation_add_static(languages, tags, translations);
label = lv_label_create(NULL);
lv_label_set_translation_tag(label, "tiger");
lv_translation_set_language("en");
TEST_ASSERT_EQUAL_STRING(lv_label_get_text(label), "The Tiger");
lv_translation_set_language("de");
TEST_ASSERT_EQUAL_STRING(lv_label_get_text(label), "Der Tiger");
lv_translation_set_language("es");
TEST_ASSERT_EQUAL_STRING(lv_label_get_text(label), "El Tigre");
/* Unknown language translates to the tag */
lv_translation_set_language("fr");
TEST_ASSERT_EQUAL_STRING(lv_label_get_text(label), "tiger");
}
void test_label_setting_text_disables_translation(void)
{
static const char * tags[] = {"tiger", NULL};
static const char * languages[] = {"en", "de", "es", NULL};
static const char * translations[] = { "The Tiger", "Der Tiger", "El Tigre" };
lv_translation_add_static(languages, tags, translations);
label = lv_label_create(NULL);
lv_label_set_translation_tag(label, "tiger");
lv_translation_set_language("en");
TEST_ASSERT_EQUAL_STRING(lv_label_get_text(label), "The Tiger");
lv_translation_set_language("de");
TEST_ASSERT_EQUAL_STRING(lv_label_get_text(label), "Der Tiger");
/* Using set text should unbind the translation tag*/
lv_label_set_text(label, "Hello world");
lv_translation_set_language("de");
TEST_ASSERT_EQUAL_STRING(lv_label_get_text(label), "Hello world");
lv_label_set_translation_tag(label, "tiger");
TEST_ASSERT_EQUAL_STRING(lv_label_get_text(label), "Der Tiger");
/* Using set text static should unbind the translation tag*/
lv_label_set_text_static(label, "Hello world");
lv_translation_set_language("en");
TEST_ASSERT_EQUAL_STRING(lv_label_get_text(label), "Hello world");
lv_label_set_translation_tag(label, "tiger");
TEST_ASSERT_EQUAL_STRING(lv_label_get_text(label), "The Tiger");
/* Using set text fmt should unbind the translation tag*/
lv_label_set_text_fmt(label, "Hello world %d", 1);
lv_translation_set_language("de");
TEST_ASSERT_EQUAL_STRING(lv_label_get_text(label), "Hello world 1");
lv_label_set_translation_tag(label, "tiger");
TEST_ASSERT_EQUAL_STRING(lv_label_get_text(label), "Der Tiger");
}
#endif
+16
View File
@@ -62,5 +62,21 @@ void test_xml_label_with_attrs(void)
TEST_ASSERT_EQUAL_SCREENSHOT("xml/lv_label.png");
}
void test_xml_label_translation_tag(void)
{
static const char * tags[] = {"tiger", NULL};
static const char * languages[] = {"en", "de", "es", NULL};
static const char * translations[] = { "The Tiger", "Der Tiger", "El Tigre" };
lv_translation_add_static(languages, tags, translations);
lv_obj_t * scr = lv_screen_active();
const char * label1_attrs[] = {
"translation_tag", "tiger",
NULL, NULL,
};
lv_obj_t * label = lv_xml_create(scr, "lv_label", label1_attrs);
lv_translation_set_language("de");
TEST_ASSERT_EQUAL_STRING(lv_label_get_text(label), "Der Tiger");
}
#endif
+1 -1
View File
@@ -14,7 +14,7 @@ Example
</enumdef>
<prop name="text" type="string" />
<prop name="text-translated" type="string" />
<prop name="translation_tag" type="string" />
<prop name="long_mode" type="enum:lv_label_long_mode" />
<prop name="bind_text">
<param name="bind_text" type="subject" />