Hello!

Le samedi 09 novembre 2024 à 14:06 +0100, Maxime Devos a écrit :
>  * Define a mapping of module->collected-strings somewhere.
>  * Let G_ add things to this mapping, during _expansion_ (syntax-case
> instead of syntax-rules will be needed).
>  * Define another macro ‘define-marked-strings’ that, during
> expansion, looks into this mapping, and from its contents constructs
> code that defines the variable.
>  
> For hygiene, to avoid state and to make it independent of the module
> system, you could try to make it in the form:
>  
> (collect-G-literals marked-strings
>   [various definitions])
> -> (begin [various definitions] (defined marked-strings '("bla"
> ...)))
>  
> There is something similar to ‘parameterize’ but for macros and
> syntax that may be useful for this, but I’m not sure if it has the
> required semantics.

With your precious help and some digging around, I produced the
attached code. Thank you!

Vivien
;; This solution uses syntax-parameters.

(define-syntax-parameter G_
  ;; G_ is replaced in the body by a call to a function named
  ;; “translate”, which is not defined here.
  (lambda (sintax)
    (syntax-violation 'G_
                      "G_ used outside of with-exported-string-literals"
                      sintax)))

(define-syntax-parameter all-string-literals
  ;; all-string-literals is replaced by the data in the form of a list
  ;; of calls to cons: (cons context-1 message-1) (cons context-2
  ;; message-2). So, the result evaluates to a list of pairs.
  (lambda (sintax)
    (syntax-violation 'all-string-literals
                      "all-string-literals used outside of with-exported-string-literals"
                      sintax)))

(define-syntax with-exported-string-literals
  ;; This big macro will evaluate its guts by replacing calls to G_
  ;; and all-string-literals.
  (lambda (sintax)
    ;; Whenever G_ expands, its arguments are saved in
    ;; collected-strings.
    (define collected-strings '())
    ;; This is a convenience function to push a new collected string
    ;; to the list.
    (define (push-collected-string! context message)
      (unless (string? message)
        (error "only literal string messages are accepted"))
      (unless (or (not context)
                  (string? context))
        (error "only optional literal string contexts are accepted"))
      (set! collected-strings (cons (cons context message)
                                    collected-strings))
      (datum->syntax #f #t))
    ;; The *push-collected-string!* transformer converts calls to a
    ;; G_-like macro by first recording the arguments, and then
    ;; replacing the call with a call to translate.
    (define *push-collected-string!*
      (lambda (sintax)
        (syntax-case sintax ()
          ((_ context-argument message-argument)
           #`(begin
               #,(push-collected-string! (syntax->datum #'context-argument)
                                         (syntax->datum #'message-argument))
               (translate message-argument #:context context-argument)))
          ((_ message-argument)
           #`(begin
               #,(push-collected-string! #f (syntax->datum #'message-argument))
               (translate message-argument))))))
    ;; The *all-collected-string* transformer converts the
    ;; all-string-literals placeholder with code evaluating to the
    ;; data: (list (cons "context-1" "message-1") (cons "context-2"
    ;; "message-2")), because I can’t get the transformer to do (quote
    ;; ("context-1" . "message-1") ...)
    (define *all-collected-strings*
      (lambda (sintax)
        (syntax-case sintax ()
          (_
           #`(list
              #,@(map
                  (lambda (item)
                    #`(cons #,(datum->syntax #f (car item))
                            #,(datum->syntax #f (cdr item))))
                  (reverse collected-strings)))))))
    ;; And now the transformation of the guts to
    ;; with-exported-string-literals.
    (syntax-case sintax ()
      ((_ body ...)
       (with-syntax ((push *push-collected-string!*)
                     (all *all-collected-strings*))
         #'(begin
             ;; It uses syntax-parameterize.
             (syntax-parameterize ((G_ push))
               ;; I can’t end the syntax-parameterize body with the
               ;; dots (wtf???) so I end it with a #t. The body does
               ;; not have access to the all-string-literals form.
               body ... #t)
             (syntax-parameterize ((all-string-literals all))
               (define _i18n:strings
                 all-string-literals)
               (export _i18n:strings))))))))

,expand (with-exported-string-literals
         (define (generic-hello)
           (display (G_ "greeting" "Hello, world!\\n"))
           (newline))
         (define (hello-guile-user)
           (display (G_ "greeting" "Hello, Guile user!\\n"))
           (newline)))

;; Expands to:
#;(begin
  (let ()
    (define (generic-hello)
      (display
        (begin
          #t ;; This is unfortunate but not a problem
          (translate
            "Hello, world!\\n"
            #:context
            "greeting")))
      (newline))
    (define (hello-guile-user)
      (display
        (begin
          #t
          (translate
            "Hello, Guile user!\\n"
            #:context
            "greeting")))
      (newline))
    #t)
  (let ()
    (define _i18n:strings ;; It is unfortunate that I can’t produce a quoted S-expr
      (list (cons "greeting" "Hello, world!\\n")
            (cons "greeting" "Hello, Guile user!\\n")))
    ;; The rest is to export _i18n:strings:
    ((@@ (guile) call-with-deferred-observers)
     (lambda ()
       ((@@ (guile) module-export!)
        ((@@ (guile) current-module))
        '(_i18n:strings))))))

Reply via email to