diff --git a/src/ace-lib/ace.ml b/src/ace-lib/ace.ml index 8d2f8f0a6..2bbe01e06 100644 --- a/src/ace-lib/ace.ml +++ b/src/ace-lib/ace.ml @@ -1,6 +1,6 @@ (* This file is part of Learn-OCaml. * - * Copyright (C) 2019 OCaml Software Foundation. + * Copyright (C) 2019-2022 OCaml Software Foundation. * Copyright (C) 2016-2018 OCamlPro. * * Learn-OCaml is distributed under the terms of the MIT license. See the @@ -20,6 +20,8 @@ type 'a editor = { editor: ('a editor * 'a option) Ace_types.editor Js.t; mutable marks: int list; mutable keybinding_menu: bool; + mutable synchronized : bool; + mutable sync_observers : (bool -> unit) list; } let ace : Ace_types.ace Js.t = Js.Unsafe.variable "ace" @@ -30,10 +32,13 @@ let create_position r c = pos##.row := r; pos##.column := c; pos + let greater_position p1 p2 = p1##.row > p2##.row || (p1##.row = p2##.row && p1##.column > p2##.column) +let register_sync_observer editor obs = + editor.sync_observers <- obs :: editor.sync_observers let create_range s e = let range : range Js.t = Js.Unsafe.obj [||] in @@ -77,15 +82,37 @@ let get_contents ?range e = let r = create_range (create_position r1 c1) (create_position r2 c2) in Js.to_string @@ document##(getTextRange r) -let create_editor editor_div = +let set_synchronized_status editor status = + List.iter (fun obs -> obs status) editor.sync_observers; + editor.synchronized <- status + +let focus { editor } = editor##focus + +let create_editor editor_div check_valid_state = let editor = edit editor_div in Js.Unsafe.set editor "$blockScrolling" (Js.Unsafe.variable "Infinity"); let data = - { editor; editor_div; marks = []; keybinding_menu = false; } in + { editor; editor_div; + marks = []; + keybinding_menu = false; + synchronized = true; + sync_observers = [] + } + in editor##.customData := (data, None); editor##setOption (Js.string "displayIndentGuides") (Js.bool false); + editor##on (Js.string "change") (fun () -> + check_valid_state (set_contents data) (fun () -> focus data) + (fun () -> set_synchronized_status data true); + set_synchronized_status data false); data +let set_synchronized editor = + set_synchronized_status editor true + +let is_synchronized editor = + editor.synchronized + let get_custom_data { editor } = match snd editor##.customData with | None -> raise Not_found @@ -168,7 +195,6 @@ let clear_marks editor = let record_event_handler editor event handler = editor.editor##(on (Js.string event) handler) -let focus { editor } = editor##focus let resize { editor } force = editor##(resize (Js.bool force)) let get_keybinding_menu e = diff --git a/src/ace-lib/ace.mli b/src/ace-lib/ace.mli index 6bc3b423e..6b922b2c1 100644 --- a/src/ace-lib/ace.mli +++ b/src/ace-lib/ace.mli @@ -17,7 +17,14 @@ type loc = { loc_end: int * int; } -val create_editor: Dom_html.divElement Js.t -> 'a editor +val create_editor: Dom_html.divElement Js.t + -> ((string -> unit) -> (unit -> unit) -> (unit -> unit) -> unit) -> 'a editor + +val is_synchronized : 'a editor -> bool + +val set_synchronized : 'a editor -> unit + +val register_sync_observer : 'a editor -> (bool -> unit) -> unit val set_mode: 'a editor -> string -> unit diff --git a/src/ace-lib/ocaml_mode.ml b/src/ace-lib/ocaml_mode.ml index c5bd1fecb..46278c8a7 100644 --- a/src/ace-lib/ocaml_mode.ml +++ b/src/ace-lib/ocaml_mode.ml @@ -514,8 +514,8 @@ let do_delete ace_editor = Ace.remove ace_editor "left" end -let create_ocaml_editor div = - let ace = Ace.create_editor div in +let create_ocaml_editor div check_valid_state = + let ace = Ace.create_editor div check_valid_state in Ace.set_mode ace "ace/mode/ocaml.ocp"; Ace.set_tab_size ace !config.indent.IndentConfig.i_base; let editor = { ace; current_error = None; current_warnings = [] } in diff --git a/src/ace-lib/ocaml_mode.mli b/src/ace-lib/ocaml_mode.mli index d2d2f78f8..c86baae05 100644 --- a/src/ace-lib/ocaml_mode.mli +++ b/src/ace-lib/ocaml_mode.mli @@ -20,12 +20,12 @@ type msg = { msg: string; } - type error = msg list type warning = error -val create_ocaml_editor: Dom_html.divElement Js.t -> editor +val create_ocaml_editor: + Dom_html.divElement Js.t -> ((string -> unit) -> (unit -> unit) -> (unit -> unit) -> unit) -> editor val get_editor: editor -> editor Ace.editor val report_error: editor -> ?set_class: bool -> error option -> warning list -> unit Lwt.t diff --git a/src/app/learnocaml_common.ml b/src/app/learnocaml_common.ml index cc8b4fcca..a7ccd0eb5 100644 --- a/src/app/learnocaml_common.ml +++ b/src/app/learnocaml_common.ml @@ -1,6 +1,6 @@ (* This file is part of Learn-OCaml. * - * Copyright (C) 2019 OCaml Software Foundation. + * Copyright (C) 2019-2020 OCaml Software Foundation. * Copyright (C) 2016-2018 OCamlPro. * * Learn-OCaml is distributed under the terms of the MIT license. See the @@ -434,10 +434,13 @@ let get_state_as_save_file ?(include_reports = false) () = all_exercise_toplevel_histories = retrieve all_exercise_toplevel_histories; } -let rec sync_save token save_file = +let rec sync_save token save_file on_sync = Server_caller.request (Learnocaml_api.Update_save (token, save_file)) >>= function - | Ok save -> set_state_from_save_file ~token save; Lwt.return save + | Ok save -> + set_state_from_save_file ~token save; + on_sync (); + Lwt.return save | Error (`Not_found _) -> Server_caller.request_exn (Learnocaml_api.Create_token ("", Some token, None)) >>= fun _token -> @@ -445,19 +448,20 @@ let rec sync_save token save_file = Server_caller.request_exn (Learnocaml_api.Update_save (token, save_file)) >>= fun save -> set_state_from_save_file ~token save; + on_sync (); Lwt.return save | Error e -> lwt_alert ~title:[%i"SYNC FAILED"] [ H.p [H.txt [%i"Could not synchronise save with the server"]]; H.code [H.txt (Server_caller.string_of_error e)]; ] ~buttons:[ - [%i"Retry"], (fun () -> sync_save token save_file); - [%i"Ignore"], (fun () -> Lwt.return save_file); + [%i"Retry"], (fun () -> sync_save token save_file on_sync); + [%i"Ignore"], (fun () -> Lwt.return save_file); ] -let sync token = sync_save token (get_state_as_save_file ()) +let sync token on_sync = sync_save token (get_state_as_save_file ()) on_sync -let sync_exercise token ?answer ?editor id = +let sync_exercise token ?answer ?editor id on_sync = let handle_serverless () = (* save the text at least locally (but not the report & grade, that could be misleading) *) @@ -494,7 +498,7 @@ let sync_exercise token ?answer ?editor id = } in match token with | Some token -> - Lwt.catch (fun () -> sync_save token save_file) + Lwt.catch (fun () -> sync_save token save_file on_sync) (fun e -> handle_serverless (); raise e) @@ -708,11 +712,72 @@ let mouseover_toggle_signal elt sigvalue setter = in Manip.Ev.onmouseover elt hdl +(* + + If a user has made no change to a solution for the exercise [id] + for 180 seconds, [check_valid_editor_state id] ensures that there is + no more recent version of this solution in the server. If this is + the case, the user is asked if we should download this solution + from the server. + + This function reduces the risk of an involuntary overwriting of a + student solution when the solution is open in several clients. + +*) +let is_synchronized_with_server_callback = ref (fun () -> false) + +let is_synchronized_with_server () = !is_synchronized_with_server_callback () + +let check_valid_editor_state id = + let last_changed = ref (Unix.gettimeofday ()) in + fun update_content focus_back on_sync -> + let update_local_copy checking_time () = + let get_solution () = + Learnocaml_local_storage.(retrieve (exercise_state id)).Answer.solution in + try let mtime = + Learnocaml_local_storage.(retrieve (exercise_state id)).Answer.mtime in + if mtime > checking_time then begin + let buttons = + if is_synchronized_with_server () then + [ + [%i "Fetch from server"], + (fun () -> let solution = get_solution () in + Lwt.return (focus_back (); update_content solution; on_sync ())); + [%i "Ignore & keep editing"], + (fun () -> Lwt.return (focus_back ())); + ] + else + [ + [%i "Ignore & keep editing"], + (fun () -> Lwt.return (focus_back ())); + [%i "Fetch from server & overwrite"], + (fun () -> let solution = get_solution () in + Lwt.return (focus_back (); update_content solution; on_sync ())); + ] + in + lwt_alert ~title:"Question" + ~buttons + [ H.p [H.txt [%i "A more recent answer exists on the server. \ + Do you want to fetch the new version?"] ] ] + end else Lwt.return_unit + with + | Not_found -> Lwt.return () + in + let now = Unix.gettimeofday () in + if now -. !last_changed > 180. then ( + let checking_time = !last_changed in + last_changed := now; + Lwt.async (update_local_copy checking_time) + ) else + last_changed := now + + let ace_display tab = let ace = lazy ( let answer = Ocaml_mode.create_ocaml_editor (Tyxml_js.To_dom.of_div tab) + (fun _ _ _ -> ()) in let ace = Ocaml_mode.get_editor answer in Ace.set_font_size ace 16; @@ -874,7 +939,8 @@ end module Editor_button (E : Editor_info) = struct - let editor_button = button ~container:E.buttons_container ~theme:"light" + let editor_button = + button ~container:E.buttons_container ~theme:"light" let cleanup template = editor_button @@ -901,16 +967,26 @@ module Editor_button (E : Editor_info) = struct select_tab "toplevel"; Lwt.return_unit - let sync token id = - editor_button + let sync token id on_sync = + let state = button_state () in + (editor_button + ~state ~icon: "sync" [%i"Sync"] @@ fun () -> token >>= fun token -> - sync_exercise token id ~editor:(Ace.get_contents E.ace) >|= fun _save -> () + sync_exercise token id ~editor:(Ace.get_contents E.ace) on_sync + >|= fun _save -> ()); + Ace.register_sync_observer E.ace (fun sync -> + if sync then disable_button state else enable_button state) + end -let setup_editor solution = +let setup_editor id solution = let editor_pane = find_component "learnocaml-exo-editor-pane" in - let editor = Ocaml_mode.create_ocaml_editor (Tyxml_js.To_dom.of_div editor_pane) in + let editor = + Ocaml_mode.create_ocaml_editor + (Tyxml_js.To_dom.of_div editor_pane) + (check_valid_editor_state id) + in let ace = Ocaml_mode.get_editor editor in Ace.set_contents ace ~reset_undo:true solution; Ace.set_font_size ace 18; @@ -1022,7 +1098,7 @@ let setup_prelude_pane ace prelude = (fun _ -> state := not !state ; update () ; true) ; Manip.appendChildren prelude_pane [ prelude_title ; prelude_container ] - + let get_token ?(has_server = true) () = if not has_server then Lwt.return None @@ -1041,7 +1117,7 @@ let get_token ?(has_server = true) () = >|= fun token -> Learnocaml_local_storage.(store sync_token) token; Some token - + module Display_exercise = functor ( Q: sig diff --git a/src/app/learnocaml_common.mli b/src/app/learnocaml_common.mli index 5ace9a36f..90d917aaa 100644 --- a/src/app/learnocaml_common.mli +++ b/src/app/learnocaml_common.mli @@ -119,18 +119,22 @@ val set_state_from_save_file : (** Gets a save file containing the locally stored data *) val get_state_as_save_file : ?include_reports:bool -> unit -> Save.t -(** Sync the local save state with the server state, and returns the merged save - file. The save will be created on the server if it doesn't exist. +(** + [sync token on_sync] synchronizes the local save state with the server state, + and returns the merged save file. The save will be created on the server + if it doesn't exist. [on_sync ()] is called when this is done. - This syncs student {b content}, but never the reports which are only synched - on "Grade" *) -val sync: Token.t -> Save.t Lwt.t + Notice that this function synchronizes student {b,content} but not the + reports which are only synchronized when an actual "grading" is done. +*) +val sync: Token.t -> (unit -> unit) -> Save.t Lwt.t (** The same, but limiting the submission to the given exercise, using the given answer if any, and the given editor text, if any. *) val sync_exercise: Token.t option -> ?answer:Learnocaml_data.Answer.t -> ?editor:string -> Learnocaml_data.Exercise.id -> + (unit -> unit) -> Save.t Lwt.t val countdown: @@ -211,10 +215,12 @@ module Editor_button (_ : Editor_info) : sig val cleanup : string -> unit val download : string -> unit val eval : Learnocaml_toplevel.t -> (string -> unit) -> unit - val sync : Token.t option Lwt.t -> Learnocaml_data.SMap.key -> unit + val sync : Token.t option Lwt.t -> Learnocaml_data.SMap.key -> (unit -> unit) -> unit end -val setup_editor : string -> Ocaml_mode.editor * Ocaml_mode.editor Ace.editor +val setup_editor : string -> string -> Ocaml_mode.editor * Ocaml_mode.editor Ace.editor + +val is_synchronized_with_server_callback : (unit -> bool) ref val typecheck : Learnocaml_toplevel.t -> diff --git a/src/app/learnocaml_exercise_main.ml b/src/app/learnocaml_exercise_main.ml index f0492a58b..bea8bde16 100644 --- a/src/app/learnocaml_exercise_main.ml +++ b/src/app/learnocaml_exercise_main.ml @@ -1,6 +1,6 @@ (* This file is part of Learn-OCaml. * - * Copyright (C) 2019 OCaml Software Foundation. + * Copyright (C) 2019-2020 OCaml Software Foundation. * Copyright (C) 2016-2018 OCamlPro. * * Learn-OCaml is distributed under the terms of the MIT license. See the @@ -81,9 +81,9 @@ module Exercise_link = ] content end - -module Display = Display_exercise(Exercise_link) -open Display + +module Display = Display_exercise(Exercise_link) +open Display let is_readonly = ref false @@ -179,10 +179,11 @@ let () = Tyxml_js.Html5.[ h1 [ txt ex_meta.Exercise.Meta.title ] ; Tyxml_js.Of_dom.of_iFrame text_iframe ] ; (* ---- editor pane --------------------------------------------------- *) - let editor, ace = setup_editor solution in + let editor, ace = setup_editor id solution in + is_synchronized_with_server_callback := (fun () -> Ace.is_synchronized ace); let module EB = Editor_button (struct let ace = ace let buttons_container = editor_toolbar end) in EB.cleanup (Learnocaml_exercise.(access File.template exo)); - EB.sync token id; + EB.sync token id (fun () -> Ace.focus ace; Ace.set_synchronized ace) ; EB.download id; EB.eval top select_tab; let typecheck = typecheck top ace editor in @@ -267,7 +268,8 @@ let () = Some solution, None in token >>= fun token -> - sync_exercise token id ?answer ?editor >>= fun _save -> + sync_exercise token id ?answer ?editor (fun () -> Ace.set_synchronized ace) + >>= fun _save -> select_tab "report" ; Lwt_js.yield () >>= fun () -> Ace.focus ace ; @@ -286,7 +288,25 @@ let () = Ace.focus ace ; typecheck true end ; - Window.onunload (fun _ev -> local_save ace id; true); + (* Small but cross-compatible hack (tested with Firefox-ESR, Chromium, Safari) + * that reuses part of this commit: + * https://github.com/pfitaxel/learn-ocaml/commit/15780b5b7c91689a26cfeaf33f3ed2cdb3a5e801 + * For details on this event, see: + * https://developer.mozilla.org/en-US/docs/Web/API/WindowEventHandlers/onbeforeunload#example + * + * Ideally, we might have wanted to use a variant of [Dom.handler] + * that is compatible with "unbeforeunload". + * For further discussion on this issue, see: + * https://github.com/ocaml-sf/learn-ocaml/issues/467 + *) + let prompt_before_unload () : unit = + Js.Unsafe.js_expr "window.onbeforeunload = function(e) {e.preventDefault(); return false;}" in + let resume_before_unload () : unit = + Js.Unsafe.js_expr "window.onbeforeunload = null" in + let () = + Ace.register_sync_observer ace (fun sync -> + if not sync then prompt_before_unload () + else resume_before_unload ()) in (* ---- return -------------------------------------------------------- *) toplevel_launch >>= fun _ -> typecheck false >>= fun () -> diff --git a/src/app/learnocaml_index_main.ml b/src/app/learnocaml_index_main.ml index d3eda82e2..1019e8bf1 100644 --- a/src/app/learnocaml_index_main.ml +++ b/src/app/learnocaml_index_main.ml @@ -815,7 +815,7 @@ let () = Lwt.return_unit); [%i"Sync workspace"], "sync", (fun () -> catch_with_alert @@ fun () -> - sync () >>= fun _ -> Lwt.return_unit); + sync () ignore >>= fun _ -> Lwt.return_unit); [%i"Export to file"], "download", download_save; [%i"Import"], "upload", import_save; [%i"Download all source files"], "download", download_all; diff --git a/src/app/learnocaml_playground_main.ml b/src/app/learnocaml_playground_main.ml index d877017b0..fd12cc7e0 100644 --- a/src/app/learnocaml_playground_main.ml +++ b/src/app/learnocaml_playground_main.ml @@ -27,8 +27,12 @@ let main () = disable_button_group toplevel_buttons_group (* enabled after init *) ; let toplevel_toolbar = find_component "learnocaml-exo-toplevel-toolbar" in let editor_toolbar = find_component "learnocaml-exo-editor-toolbar" in - let toplevel_button = - button ~container: toplevel_toolbar ~theme: "dark" ~group:toplevel_buttons_group ?state:None in + let toplevel_button ~icon label cb = + ignore @@ + button + ~icon ~container: toplevel_toolbar + ~theme: "dark" ~group:toplevel_buttons_group ?state:None label cb + in let id = match Url.Current.path with | "" :: "playground" :: p | "playground" :: p -> String.concat "/" (List.map Url.urldecode (List.filter ((<>) "") p)) @@ -60,7 +64,7 @@ let main () = (* ---- toplevel pane ------------------------------------------------- *) init_toplevel_pane toplevel_launch top toplevel_buttons_group toplevel_button ; (* ---- editor pane --------------------------------------------------- *) - let editor, ace = setup_editor solution in + let editor, ace = setup_editor id solution in let module EB = Editor_button (struct let ace = ace let buttons_container = editor_toolbar end) in EB.cleanup playground.Playground.template; EB.download id; diff --git a/translations/fr.po b/translations/fr.po index d84b10f73..7aae463af 100644 --- a/translations/fr.po +++ b/translations/fr.po @@ -1,4 +1,5 @@ # LEARN-OCAML FRENCH TRANSLATION +# Copyright (C) 2020 OCaml Software Foundation. # Copyright (C) 2018 OCamlPro # Louis Gesbert , 2018. # @@ -99,197 +100,217 @@ msgstr "ERREUR DE REQUÊTE" msgid "Could not retrieve data from server" msgstr "Échec lors du téléchargement des données du serveur" -#: File "src/app/learnocaml_common.ml", line 414, characters 12-19 454, 11-18 +#: File "src/app/learnocaml_common.ml", line 414, characters 12-19 458, 13-20 #: "src/app/learnocaml_index_main.ml", 582, 17-24 msgid "Retry" msgstr "Réessayer" -#: File "src/app/learnocaml_common.ml", line 417, characters 25-33 455, 11-19 +#: File "src/app/learnocaml_common.ml", line 417, characters 25-33 459, 13-21 msgid "Ignore" msgstr "Ignorer" -#: File "src/app/learnocaml_common.ml", line 450, characters 26-39 +#: File "src/app/learnocaml_common.ml", line 454, characters 26-39 msgid "SYNC FAILED" msgstr "ECHEC DE LA SYNCHRONISATION" -#: File "src/app/learnocaml_common.ml", line 451, characters 22-66 +#: File "src/app/learnocaml_common.ml", line 455, characters 22-66 msgid "Could not synchronise save with the server" msgstr "Les données n'ont pas pu être synchronisées avec le serveur" -#: File "src/app/learnocaml_common.ml", line 510, characters 39-50 +#: File "src/app/learnocaml_common.ml", line 514, characters 39-50 msgid "%dd %02dh" msgstr "%dj %02dh" -#: File "src/app/learnocaml_common.ml", line 511, characters 40-51 +#: File "src/app/learnocaml_common.ml", line 515, characters 40-51 msgid "%02d:%02d" msgstr "%02d:%02d" -#: File "src/app/learnocaml_common.ml", line 512, characters 23-36 +#: File "src/app/learnocaml_common.ml", line 516, characters 23-36 msgid "0:%02d:%02d" msgstr "0:%02d:%02d" -#: File "src/app/learnocaml_common.ml", line 543, characters 34-55 1071, 38-59 +#: File "src/app/learnocaml_common.ml", line 547, characters 34-55 1147, 38-59 msgid "difficulty: %d / 40" msgstr "difficulté: %d / 40" -#: File "src/app/learnocaml_common.ml", line 578, characters 30-75 +#: File "src/app/learnocaml_common.ml", line 582, characters 30-75 msgid "No description available for this exercise." msgstr "Aucune description pour cet exercice." -#: File "src/app/learnocaml_common.ml", line 601, characters 32-41 +#: File "src/app/learnocaml_common.ml", line 605, characters 32-41 #: "src/app/learnocaml_index_main.ml", 132, 54-63 msgid "project" msgstr "projet" -#: File "src/app/learnocaml_common.ml", line 602, characters 32-41 +#: File "src/app/learnocaml_common.ml", line 606, characters 32-41 #: "src/app/learnocaml_index_main.ml", 133, 54-63 msgid "problem" msgstr "problème" -#: File "src/app/learnocaml_common.ml", line 603, characters 33-43 +#: File "src/app/learnocaml_common.ml", line 607, characters 33-43 #: "src/app/learnocaml_index_main.ml", 134, 55-65 msgid "exercise" msgstr "exercice" -#: File "src/app/learnocaml_common.ml", line 755, characters 26-33 +#: File "src/app/learnocaml_common.ml", line 743, characters 20-39 +msgid "Fetch from server" +msgstr "Télécharger du serveur" + +#: File "src/app/learnocaml_common.ml", line 746, characters 20-43 751, +msgid "Ignore & keep editing" +msgstr "Ignorer & continuer d'éditer" + +#: File "src/app/learnocaml_common.ml", line 753, characters 20-51 +msgid "Fetch from server & overwrite" +msgstr "Télécharger du serveur & écraser" + +#: File "src/app/learnocaml_common.ml", lines 760-761, characters 28-67 +msgid "" +"A more recent answer exists on the server. Do you want to fetch the new " +"version?" +msgstr "" +"Une version plus récente de cette réponse existe sur le serveur. Voulez-vous " +"télécharger la nouvelle version ?" + +#: File "src/app/learnocaml_common.ml", line 820, characters 26-33 msgid "Clear" msgstr "Effacer" -#: File "src/app/learnocaml_common.ml", line 760, characters 25-32 881, 24-31 +#: File "src/app/learnocaml_common.ml", line 825, characters 25-32 947, 24-31 msgid "Reset" msgstr "Réinitialiser" -#: File "src/app/learnocaml_common.ml", line 765, characters 22-35 +#: File "src/app/learnocaml_common.ml", line 830, characters 22-35 msgid "Eval phrase" msgstr "Évaluer la phrase" -#: File "src/app/learnocaml_common.ml", line 780, characters 24-51 +#: File "src/app/learnocaml_common.ml", line 845, characters 24-51 msgid "Preparing the environment" msgstr "Préparation de l'environnement" -#: File "src/app/learnocaml_common.ml", line 781, characters 39-47 786, 37-45 +#: File "src/app/learnocaml_common.ml", line 846, characters 39-47 851, 37-45 msgid "Editor" msgstr "Éditeur" -#: File "src/app/learnocaml_common.ml", line 782, characters 41-51 +#: File "src/app/learnocaml_common.ml", line 847, characters 41-51 #: "src/app/learnocaml_index_main.ml", 691, 30-40 msgid "Toplevel" msgstr "Toplevel" -#: File "src/app/learnocaml_common.ml", line 783, characters 39-47 795, +#: File "src/app/learnocaml_common.ml", line 848, characters 39-47 860, #: "src/app/learnocaml_exercise_main.ml", 58, 30-38 62, 67, #: "src/app/learnocaml_student_view.ml", 383, 28-36 396, 400, 405, msgid "Report" msgstr "Rapport" -#: File "src/app/learnocaml_common.ml", line 784, characters 37-47 +#: File "src/app/learnocaml_common.ml", line 849, characters 37-47 msgid "Exercise" msgstr "Exercice" -#: File "src/app/learnocaml_common.ml", line 785, characters 37-46 +#: File "src/app/learnocaml_common.ml", line 850, characters 37-46 msgid "Details" msgstr "Détails" -#: File "src/app/learnocaml_common.ml", line 787, characters 27-70 +#: File "src/app/learnocaml_common.ml", line 852, characters 27-70 msgid "Click the Grade button to get your report" msgstr "Cliquez sur le bouton Noter pour obtenir votre rapport" -#: File "src/app/learnocaml_common.ml", line 792, characters 22-44 +#: File "src/app/learnocaml_common.ml", line 857, characters 22-44 msgid "Loading student data" msgstr "Chargement des informations sur les étudiants" -#: File "src/app/learnocaml_common.ml", line 793, characters 38-45 +#: File "src/app/learnocaml_common.ml", line 858, characters 38-45 msgid "Stats" msgstr "Statistiques" -#: File "src/app/learnocaml_common.ml", line 794, characters 37-48 +#: File "src/app/learnocaml_common.ml", line 859, characters 37-48 #: "src/app/learnocaml_index_main.ml", 688, 29-40 #: "src/app/learnocaml_teacher_tab.ml", 329, 18-29 -#: "src/app/learnocaml_exercise_main.ml", 202, 23-34 +#: "src/app/learnocaml_exercise_main.ml", 203, 23-34 msgid "Exercises" msgstr "Exercices" -#: File "src/app/learnocaml_common.ml", line 796, characters 37-46 +#: File "src/app/learnocaml_common.ml", line 861, characters 37-46 msgid "Subject" msgstr "Énoncé" -#: File "src/app/learnocaml_common.ml", line 797, characters 39-47 +#: File "src/app/learnocaml_common.ml", line 862, characters 39-47 msgid "Answer" msgstr "Réponse" -#: File "src/app/learnocaml_common.ml", line 882, characters 22-42 +#: File "src/app/learnocaml_common.ml", line 948, characters 22-42 msgid "START FROM SCRATCH" msgstr "TOUT RECOMMENCER" -#: File "src/app/learnocaml_common.ml", line 883, characters 16-65 +#: File "src/app/learnocaml_common.ml", line 949, characters 16-65 msgid "This will discard all your edits. Are you sure?" msgstr "Toutes vos modifications seront perdues. Vous êtes sûr·e ?" -#: File "src/app/learnocaml_common.ml", line 890, characters 27-37 +#: File "src/app/learnocaml_common.ml", line 956, characters 27-37 msgid "Download" msgstr "Télécharger" -#: File "src/app/learnocaml_common.ml", line 898, characters 22-33 +#: File "src/app/learnocaml_common.ml", line 964, characters 22-33 msgid "Eval code" msgstr "Exécuter le code" -#: File "src/app/learnocaml_common.ml", line 906, characters 23-29 +#: File "src/app/learnocaml_common.ml", line 974, characters 23-29 msgid "Sync" msgstr "Sync" -#: File "src/app/learnocaml_common.ml", line 964, characters 34-49 999, +#: File "src/app/learnocaml_common.ml", line 1040, characters 34-49 1075, msgid "OCaml prelude" msgstr "Prélude OCaml" -#: File "src/app/learnocaml_common.ml", line 971, characters 59-65 1006, +#: File "src/app/learnocaml_common.ml", line 1047, characters 59-65 1082, msgid "Hide" msgstr "Cacher" -#: File "src/app/learnocaml_common.ml", line 976, characters 59-65 1013, +#: File "src/app/learnocaml_common.ml", line 1052, characters 59-65 1089, msgid "Show" msgstr "Montrer" -#: File "src/app/learnocaml_common.ml", line 1037, characters 18-36 +#: File "src/app/learnocaml_common.ml", line 1113, characters 18-36 msgid "Enter the secret" msgstr "Entrez le secret" -#: File "src/app/learnocaml_common.ml", line 1077, characters 22-35 +#: File "src/app/learnocaml_common.ml", line 1153, characters 22-35 msgid "Difficulty:" msgstr "Difficulté :" -#: File "src/app/learnocaml_common.ml", line 1091, characters 39-49 +#: File "src/app/learnocaml_common.ml", line 1167, characters 39-49 msgid "Kind: %s" msgstr "Type : %s" -#: File "src/app/learnocaml_common.ml", line 1232, characters 46-59 +#: File "src/app/learnocaml_common.ml", line 1308, characters 46-59 msgid "Identifier:" msgstr "Identifiant de l'exercice :" -#: File "src/app/learnocaml_common.ml", line 1236, characters 48-57 +#: File "src/app/learnocaml_common.ml", line 1312, characters 48-57 msgid "Author:" msgstr "Auteur :" -#: File "src/app/learnocaml_common.ml", line 1237, characters 47-57 +#: File "src/app/learnocaml_common.ml", line 1313, characters 47-57 msgid "Authors:" msgstr "Auteurs :" -#: File "src/app/learnocaml_common.ml", line 1242, characters 31-48 +#: File "src/app/learnocaml_common.ml", line 1318, characters 31-48 msgid "Skills trained:" msgstr "Compétences pratiquées :" -#: File "src/app/learnocaml_common.ml", line 1246, characters 31-49 +#: File "src/app/learnocaml_common.ml", line 1322, characters 31-49 msgid "Skills required:" msgstr "Compétences requises :" -#: File "src/app/learnocaml_common.ml", line 1251, characters 36-57 +#: File "src/app/learnocaml_common.ml", line 1327, characters 36-57 msgid "Previous exercises:" msgstr "Exercices précédents :" -#: File "src/app/learnocaml_common.ml", line 1254, characters 35-52 +#: File "src/app/learnocaml_common.ml", line 1330, characters 35-52 msgid "Next exercises:" msgstr "Exercices suivants :" -#: File "src/app/learnocaml_common.ml", line 1259, characters 26-36 +#: File "src/app/learnocaml_common.ml", line 1335, characters 26-36 msgid "Metadata" msgstr "Métadonnées" @@ -517,7 +538,7 @@ msgid "Lessons" msgstr "Cours" #: File "src/app/learnocaml_index_main.ml", line 693, characters 32-44 -#: "src/app/learnocaml_playground_main.ml", 73, 23-35 +#: "src/app/learnocaml_playground_main.ml", 77, 23-35 msgid "Playground" msgstr "Bac-à-sable" @@ -725,45 +746,46 @@ msgstr "" "seront plus sauvegardés sur le serveur." #: File "src/app/learnocaml_exercise_main.ml", line 130, characters 25-49 -#: "src/app/learnocaml_playground_main.ml", 43, 19-43 +#: "src/app/learnocaml_playground_main.ml", 47, 19-43 msgid "loading the prelude..." msgstr "Chargement du prélude..." #: File "src/app/learnocaml_exercise_main.ml", line 135, characters 41-59 -#: "src/app/learnocaml_playground_main.ml", 46, 31-49 +#: "src/app/learnocaml_playground_main.ml", 50, 31-49 msgid "error in prelude" msgstr "erreur dans le prélude" -#: File "src/app/learnocaml_exercise_main.ml", line 214, characters 28-37 -#: "src/app/learnocaml_playground_main.ml", 80, +#: File "src/app/learnocaml_exercise_main.ml", line 215, characters 28-37 +#: "src/app/learnocaml_playground_main.ml", 84, msgid "Compile" msgstr "Compiler" -#: File "src/app/learnocaml_exercise_main.ml", line 218, characters 29-37 +#: File "src/app/learnocaml_exercise_main.ml", line 219, characters 29-37 msgid "Grade!" msgstr "Noter!" -#: File "src/app/learnocaml_exercise_main.ml", line 222, characters 48-55 +#: File "src/app/learnocaml_exercise_main.ml", line 223, characters 48-55 msgid "abort" msgstr "abandonner" -#: File "src/app/learnocaml_exercise_main.ml", lines 226-227, characters 35-65 +#: File "src/app/learnocaml_exercise_main.ml", lines 227-228, characters 35-65 msgid "Grading is taking a lot of time, maybe your code is looping? " -msgstr "La notation prend du temps, peut-être une boucle infinie dans votre code ? " +msgstr "" +"La notation prend du temps, peut-être une boucle infinie dans votre code ? " -#: File "src/app/learnocaml_exercise_main.ml", line 233, characters 35-57 +#: File "src/app/learnocaml_exercise_main.ml", line 234, characters 35-57 msgid "Launching the grader" msgstr "Lancement de la notation" -#: File "src/app/learnocaml_exercise_main.ml", line 256, characters 60-86 +#: File "src/app/learnocaml_exercise_main.ml", line 257, characters 60-86 msgid "Grading aborted by user." msgstr "Notation annulée par l'utilisateur." -#: File "src/app/learnocaml_exercise_main.ml", line 277, characters 38-59 +#: File "src/app/learnocaml_exercise_main.ml", line 279, characters 38-59 msgid "Error in your code." msgstr "Erreur dans le code." -#: File "src/app/learnocaml_exercise_main.ml", line 278, characters 27-85 +#: File "src/app/learnocaml_exercise_main.ml", line 280, characters 27-85 msgid "Cannot start the grader if your code does not typecheck." msgstr "La notation ne peut être lancée si le code ne compile pas." @@ -893,7 +915,7 @@ msgstr "" "%a\n" "%!" -#: File "src/grader/grading.ml", line 96, characters 38-65 106, 131, 139, +#: File "src/grader/grading.ml", line 96, characters 38-65 106, 131, 139, 143, msgid "while preparing the tests" msgstr "lors de la préparation des tests" @@ -925,15 +947,15 @@ msgstr "lors du chargement de la solution" msgid "Preparing to launch the tests." msgstr "Préparation du lancement des tests." -#: File "src/grader/grading.ml", line 142, characters 22-49 +#: File "src/grader/grading.ml", line 146, characters 22-49 msgid "Launching the test bench." msgstr "Lancement du banc de test." -#: File "src/grader/grading.ml", line 171, characters 45-78 +#: File "src/grader/grading.ml", line 175, characters 45-78 msgid "while loading user dependencies" msgstr "lors du chargement des dépendances" -#: File "src/grader/grading.ml", line 187, characters 38-67 +#: File "src/grader/grading.ml", line 191, characters 38-67 msgid "while testing your solution" msgstr "lors du test de la solution utilisateur" @@ -945,6 +967,9 @@ msgstr "lors du test de la solution utilisateur" #~ "Le téléchargement de l'archive a échoué. Veuillez réessayer " #~ "ulterieurement!" +#~ msgid "Do you want to save your work before closing?" +#~ msgstr "Souhaitez-vous enregistrer votre travail avant de quitter?" + #~ msgid "No description available." #~ msgstr "Aucune description."