Add CHANGE_LANGUAGE feature to teacher settings

Reorders the left sidebar so risky actions sit at the bottom with a gap:
Dashboard, Synchronize, Logout | Change Language (red), Delete Account.

The new Change Language button opens a warning popup with a language
dropdown. On confirm, if the selected locale differs from the current
one, the client calls POST /reset_language. On success it wipes the
local teacher folder (progression, remediation, confusion matrix…),
applies the new language to device and teacher settings, logs out and
reloads from the splash screen. On error the user sees an error popup
and nothing local changes.
This commit is contained in:
Adrien Ufferte
2026-04-17 14:18:32 +02:00
parent ee6d7d9626
commit 790260e2fd
7 changed files with 241 additions and 6 deletions
+3
View File
@@ -434,6 +434,9 @@ CHECKING_DATA,Vérification des données...,Comprobando datos...,Verificação d
TEACHER_SETTINGS_HELP,"Pour accéder aux paramètres, veuillez appuyer sur le plus et la barre verticale, puis maintenez le bouton ""Paramètres"" pendant 5 secondes","Para acceder a los ajustes, pulse el signo más y la barra vertical y, a continuación, mantenga pulsado el botón «Configuración» durante 5 segundos","Para acessar as configurações, pressione o sinal de mais e a barra vertical e, em seguida, mantenha pressionado o botão ""Parâmetros"" por 5 segundos","Per accedere alle impostazioni, premi il segno più e la barra verticale, quindi tieni premuto il pulsante ""Impostazioni"" per 5 secondi"
DELETE_ACCOUNT,Supprimer le compte,Eliminar cuenta,Excluir conta,Elimina account
DELETE_ACCOUNT_POPUP,Voulez-vous vraiment supprimer votre compte ? Cette action est irréversible !,¿De verdad quieres eliminar tu cuenta? ¡Esta acción es irreversible!,Você realmente deseja excluir sua conta? Essa ação é irreversível!,Vuoi davvero eliminare il tuo account? Questa azione è irreversibile!
CHANGE_LANGUAGE,Changer la langue,Cambiar el idioma,Mudar o idioma,Cambia lingua
CHANGE_LANGUAGE_POPUP,"Changer la langue du compte supprimera toute la progression et les données de tous les élèves. Cette action est irréversible. Sélectionnez la nouvelle langue ci-dessous.","Cambiar el idioma de la cuenta borrará todo el progreso y los datos de todos los alumnos. Esta acción es irreversible. Seleccione el nuevo idioma a continuación.","Mudar o idioma da conta apagará todo o progresso e os dados de todos os alunos. Essa ação é irreversível. Selecione o novo idioma abaixo.","Cambiare la lingua dell'account cancellerà tutti i progressi e i dati di tutti gli studenti. Questa azione è irreversibile. Seleziona di seguito la nuova lingua."
CHANGE_LANGUAGE_ERROR,"Impossible de changer la langue. Veuillez vérifier votre connexion et réessayer.","No se pudo cambiar el idioma. Compruebe su conexión y vuelva a intentarlo.","Não foi possível mudar o idioma. Verifique sua conexão e tente novamente.","Impossibile cambiare la lingua. Verifica la connessione e riprova."
DISCONNECTED_ERROR,Vous avez été déconnecté. Veuillez renseigner vos identifiants à nouveau.,Se ha cerrado la sesión. Vuelva a introducir sus datos de acceso.,Você foi desconectado. Digite seus detalhes de login novamente.,Sei stato disconnesso. Inserisci nuovamente le tue credenziali.
INVALID_LANGUAGE_DIRECTORY,Dossier de langue invalide,Carpeta de idioma no válida,Pasta de idioma inválida,Cartella lingua non valida
NO_INTERNET_ACCESS,Vous n'êtes pas connecté à Internet. Veuillez vérifier votre connexion et réessayer.,No está conectado a Internet. Compruebe su conexión e inténtelo de nuevo.,Você não está conectado à Internet. Verifique sua conexão e tente novamente.,Non sei connesso a Internet. Verifica la connessione e riprova.
Can't render this file because it contains an unexpected character in line 41 and column 273.
@@ -3,6 +3,7 @@ extends Control
const MAIN_MENU_PATH: String = "res://sources/menus/main/main_menu.tscn"
const LOGIN_MENU_PATH: String = "res://sources/menus/login/login.tscn"
const SPLASH_SCREEN_PATH: String = "res://sources/menus/splash_screen/splash_screen.tscn"
const DEVICE_SELECTION_SCENE_PATH: String = "res://sources/menus/device_selection/device_selection.tscn"
const DEVICE_TAB_SCENE: PackedScene = preload("res://sources/menus/settings/device_tab.tscn")
const PASSWORD_VISUALIZER_SCENE: PackedScene = preload("res://sources/menus/components/password_visualizer.tscn")
@@ -18,6 +19,8 @@ var last_device_id: int = -1
@onready var devices_tab_container: TabContainer = %DevicesTabContainer
@onready var lesson_unlocks: LessonUnlocks = $LessonUnlocks
@onready var delete_popup: ConfirmPopup = %DeletePopup
@onready var change_language_popup: ChangeLanguagePopup = %ChangeLanguagePopup
@onready var change_language_error_popup: ConfirmPopup = %ChangeLanguageErrorPopup
@onready var loading_popup: LoadingPopup = %LoadingPopup
@onready var account_type_option_button: OptionButton = %AccountTypeOptionButton
@onready var education_method_option_button: OptionButton = %EducationMethodOptionButton
@@ -125,6 +128,36 @@ func _on_logout_button_pressed() -> void:
get_tree().change_scene_to_file(MAIN_MENU_PATH)
func _on_change_language_button_pressed() -> void:
var current_language: String = UserDataManager.get_language()
change_language_popup.show_for_current_language(current_language)
func _on_change_language_popup_accepted(new_language: String) -> void:
if not new_language:
Log.warn("SettingsTeacherSettings: Change language cancelled - no language selected")
return
var current_language: String = UserDataManager.get_language()
if new_language == current_language:
Log.info("SettingsTeacherSettings: Change language skipped - selected language matches current (%s)" % current_language)
return
Log.warn("SettingsTeacherSettings: Change language from %s to %s" % [current_language, new_language])
var res: Dictionary = await ServerManager.reset_language(new_language)
if res.code != 200:
Log.error("SettingsTeacherSettings: Reset language request failed. Error code %d" % res.code)
change_language_error_popup.show()
return
# Server confirmed: wipe all local teacher data, apply new language, and restart from splash.
UserDataManager.delete_teacher_data()
if UserDataManager.teacher_settings:
UserDataManager.teacher_settings.server_language_validated = false
UserDataManager.set_language(new_language, true)
UserDataManager.logout()
get_tree().change_scene_to_file(SPLASH_SCREEN_PATH)
func _on_devices_tab_container_tab_changed(tab: int) -> void:
var device_tab: DeviceTab = devices_tab_container.get_tab_control(tab) as DeviceTab
if not device_tab:
+28 -6
View File
@@ -10,6 +10,7 @@
[ext_resource type="Theme" uid="uid://dqjvrt5nrtwn1" path="res://resources/themes/kalulu_theme.tres" id="8_7h0sa"]
[ext_resource type="PackedScene" uid="uid://qsvbq6ruc44v" path="res://sources/menus/settings/lesson_unlocks.tscn" id="9_mljfd"]
[ext_resource type="PackedScene" uid="uid://dhowigt8un22u" path="res://sources/ui/loading_popup.tscn" id="10_i488h"]
[ext_resource type="PackedScene" path="res://sources/ui/change_language_popup.tscn" id="11_chglg"]
[node name="TeacherSettings" type="Control" unique_id=149147235]
layout_mode = 3
@@ -71,6 +72,30 @@ theme_type_variation = &"FlatButton"
theme_override_font_sizes/font_size = 40
text = "SYNCHRONIZE_ACCOUNT"
[node name="LogoutButton" type="Button" parent="InterfaceLeft/Container" unique_id=808130432]
layout_mode = 2
theme_override_font_sizes/font_size = 40
text = "LOGOUT"
[node name="DangerSpacer" type="Control" parent="InterfaceLeft/Container" unique_id=312700845]
custom_minimum_size = Vector2(0, 100)
layout_mode = 2
[node name="ChangeLanguageButton" type="Button" parent="InterfaceLeft/Container" unique_id=144215823]
layout_mode = 2
theme_type_variation = &"DangerButton"
theme_override_font_sizes/font_size = 40
text = "CHANGE_LANGUAGE"
[node name="ChangeLanguagePopup" parent="InterfaceLeft/Container/ChangeLanguageButton" unique_id=144215824 instance=ExtResource("11_chglg")]
unique_name_in_owner = true
visible = false
[node name="ChangeLanguageErrorPopup" parent="InterfaceLeft/Container/ChangeLanguageButton" unique_id=144215825 instance=ExtResource("7_jetb7")]
unique_name_in_owner = true
visible = false
content_text = "CHANGE_LANGUAGE_ERROR"
[node name="DeleteButton" type="Button" parent="InterfaceLeft/Container" unique_id=721919922]
layout_mode = 2
theme_type_variation = &"DangerButton"
@@ -82,11 +107,6 @@ unique_name_in_owner = true
visible = false
content_text = "DELETE_ACCOUNT_POPUP"
[node name="LogoutButton" type="Button" parent="InterfaceLeft/Container" unique_id=808130432]
layout_mode = 2
theme_override_font_sizes/font_size = 40
text = "LOGOUT"
[node name="VBoxContainer" type="VBoxContainer" parent="." unique_id=2087046145]
custom_minimum_size = Vector2(1970, 1800)
layout_mode = 1
@@ -243,9 +263,11 @@ access = 2
[connection signal="pressed" from="InterfaceLeft/Container/BackButton" to="." method="_on_back_button_pressed"]
[connection signal="pressed" from="InterfaceLeft/Container/Dashboard" to="." method="_on_dashboard_button_pressed"]
[connection signal="pressed" from="InterfaceLeft/Container/SynchronizeButton" to="." method="_on_synchronize_button_pressed"]
[connection signal="pressed" from="InterfaceLeft/Container/LogoutButton" to="." method="_on_logout_button_pressed"]
[connection signal="pressed" from="InterfaceLeft/Container/ChangeLanguageButton" to="." method="_on_change_language_button_pressed"]
[connection signal="accepted" from="InterfaceLeft/Container/ChangeLanguageButton/ChangeLanguagePopup" to="." method="_on_change_language_popup_accepted"]
[connection signal="pressed" from="InterfaceLeft/Container/DeleteButton" to="." method="_on_delete_button_pressed"]
[connection signal="accepted" from="InterfaceLeft/Container/DeleteButton/DeletePopup" to="." method="_on_delete_popup_accepted"]
[connection signal="pressed" from="InterfaceLeft/Container/LogoutButton" to="." method="_on_logout_button_pressed"]
[connection signal="item_selected" from="VBoxContainer/HBoxContainer/AccountTypeOptionButton" to="." method="_on_account_type_option_button_item_selected"]
[connection signal="item_selected" from="VBoxContainer/HBoxContainer/EducationMethodOptionButton" to="." method="_on_education_method_option_button_item_selected"]
[connection signal="tab_changed" from="VBoxContainer/DevicesTabContainer" to="." method="_on_devices_tab_container_tab_changed"]
+76
View File
@@ -0,0 +1,76 @@
@tool
class_name ChangeLanguagePopup
extends CanvasLayer
signal accepted(language: String)
signal refused()
@export_multiline var content_text: String = "CHANGE_LANGUAGE_POPUP": set = _set_content_text
@export var close_on_action: bool = true
var _locales: Array[String] = []
@onready var content_label: Label = %ChangeLanguageContentLabel
@onready var language_field: OptionButton = %ChangeLanguageLanguageField
@onready var confirm_button: Button = %ChangeLanguageConfirmButton
@onready var cancel_button: Button = %ChangeLanguageCancelButton
func _ready() -> void:
_set_content_text(content_text)
if not Engine.is_editor_hint():
_populate_language_field()
func show_for_current_language(current_language: String) -> void:
_populate_language_field(current_language)
show()
func get_selected_language() -> String:
if not language_field:
return ""
var selected_id: int = language_field.get_selected_id()
if selected_id < 0 or selected_id >= _locales.size():
return ""
return _locales[selected_id]
func _populate_language_field(preselected_locale: String = "") -> void:
if not language_field:
return
_locales.clear()
language_field.clear()
var index: int = 0
for locale: String in Utils.SUPPORTED_LOCALES.keys():
if not locale:
continue
var locale_name: String = Utils.SUPPORTED_LOCALES[locale]
if not locale_name:
continue
language_field.add_item(locale_name, index)
_locales.append(locale)
if locale == preselected_locale:
language_field.select(index)
index += 1
if language_field.get_selected_id() == -1 and _locales.size() > 0:
language_field.select(0)
func _set_content_text(p_content_text: String) -> void:
content_text = p_content_text
if content_label:
content_label.text = content_text
func _on_confirm_button_pressed() -> void:
var selected_language: String = get_selected_language()
accepted.emit(selected_language)
if close_on_action:
hide()
func _on_cancel_button_pressed() -> void:
refused.emit()
if close_on_action:
hide()
+1
View File
@@ -0,0 +1 @@
uid://bxqtlx38skcu3
+92
View File
@@ -0,0 +1,92 @@
[gd_scene format=3]
[ext_resource type="Script" path="res://sources/ui/change_language_popup.gd" id="1_chglg"]
[ext_resource type="Theme" path="res://resources/themes/kalulu_theme.tres" id="2_chglg"]
[sub_resource type="Gradient" id="Gradient_chglg"]
colors = PackedColorArray(0.2, 0.2, 0.2, 0.505882, 0.2, 0.2, 0.2, 0.505882)
[sub_resource type="GradientTexture1D" id="GradientTexture1D_chglg"]
gradient = SubResource("Gradient_chglg")
[node name="ChangeLanguagePopup" type="CanvasLayer"]
script = ExtResource("1_chglg")
[node name="TextureRect" type="TextureRect" parent="."]
top_level = true
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
mouse_filter = 0
texture = SubResource("GradientTexture1D_chglg")
[node name="PanelContainer" type="PanelContainer" parent="TextureRect"]
layout_mode = 1
anchors_preset = 8
anchor_left = 0.5
anchor_top = 0.5
anchor_right = 0.5
anchor_bottom = 0.5
offset_left = -20.0
offset_top = -20.0
offset_right = 20.0
offset_bottom = 20.0
grow_horizontal = 2
grow_vertical = 2
theme_type_variation = &"PanelKalulu"
[node name="VBoxContainer" type="VBoxContainer" parent="TextureRect/PanelContainer"]
layout_mode = 2
[node name="MarginContainer" type="MarginContainer" parent="TextureRect/PanelContainer/VBoxContainer"]
layout_mode = 2
theme_override_constants/margin_left = 50
theme_override_constants/margin_top = 50
theme_override_constants/margin_right = 50
theme_override_constants/margin_bottom = 25
[node name="VBoxContainer" type="VBoxContainer" parent="TextureRect/PanelContainer/VBoxContainer/MarginContainer"]
layout_mode = 2
theme_override_constants/separation = 30
[node name="ChangeLanguageContentLabel" type="Label" parent="TextureRect/PanelContainer/VBoxContainer/MarginContainer/VBoxContainer"]
unique_name_in_owner = true
custom_minimum_size = Vector2(1200, 0)
layout_mode = 2
size_flags_vertical = 1
theme_override_font_sizes/font_size = 60
text = "CHANGE_LANGUAGE_POPUP"
autowrap_mode = 2
[node name="ChangeLanguageLanguageField" type="OptionButton" parent="TextureRect/PanelContainer/VBoxContainer/MarginContainer/VBoxContainer"]
unique_name_in_owner = true
custom_minimum_size = Vector2(0, 120)
layout_mode = 2
theme = ExtResource("2_chglg")
theme_override_font_sizes/font_size = 60
autowrap_mode = 2
[node name="HBoxContainer" type="HBoxContainer" parent="TextureRect/PanelContainer/VBoxContainer/MarginContainer/VBoxContainer"]
layout_mode = 2
[node name="ChangeLanguageConfirmButton" type="Button" parent="TextureRect/PanelContainer/VBoxContainer/MarginContainer/VBoxContainer/HBoxContainer"]
unique_name_in_owner = true
layout_mode = 2
theme_type_variation = &"DangerButton"
theme_override_font_sizes/font_size = 70
text = "VALIDATE"
[node name="Separator" type="Control" parent="TextureRect/PanelContainer/VBoxContainer/MarginContainer/VBoxContainer/HBoxContainer"]
layout_mode = 2
size_flags_horizontal = 3
[node name="ChangeLanguageCancelButton" type="Button" parent="TextureRect/PanelContainer/VBoxContainer/MarginContainer/VBoxContainer/HBoxContainer"]
unique_name_in_owner = true
layout_mode = 2
theme_override_font_sizes/font_size = 70
text = "CANCEL"
[connection signal="pressed" from="TextureRect/PanelContainer/VBoxContainer/MarginContainer/VBoxContainer/HBoxContainer/ChangeLanguageConfirmButton" to="." method="_on_confirm_button_pressed"]
[connection signal="pressed" from="TextureRect/PanelContainer/VBoxContainer/MarginContainer/VBoxContainer/HBoxContainer/ChangeLanguageCancelButton" to="." method="_on_cancel_button_pressed"]
@@ -174,6 +174,14 @@ func set_user_language(language: String) -> Dictionary:
await _post_request("set_language", data)
return _response()
func reset_language(language: String) -> Dictionary:
loading_rect.show()
Log.warn("ServerManager: Reset language request initiated for %s" % language)
var data: Dictionary = {"language": language}
await _post_request("reset_language", data)
return _response()
#region Sender functions
func check_internet_access() -> bool: