Hacker News new | ask | show | jobs
by somat 234 days ago
js can't use a string as a template.

my example: a table to lookup translated templates. most translation engines require you to use placeholder strings. this lets you use the template directly as the optional lookup key.

simplified with some liberties taken as this can't be done with template literals. Easy enough to fake with some regexes and loops. but I was a bit surprised that the built in js templates are limited in this manner.

    const translate_table = {
      'where is the ${thing}':'${thing} はどこですか' ,
      }

  function t(template, args) {
    if (translate_table[template] == undefined) {
      return template.format(args);
    }
    else {
     return translate_table[template].format(args);
     }
    }

    user_dialog(t('Where is the ${thing}', {'thing', users_thing} ));
I even dug deep into tagged templates, but they can't do this ether. The only solution I found was a variant of eval() and at that point I would rather write my own template engine.
2 comments

I think I understand what you're suggesting, and I think it can be achieved with javascript template literals. It might be easier to understand with a usage example instead of an implementation example.

The only restriction may be that variable placeholders in additional translations might need to be positional rather than named.

You can make your tagged template literal return an array of tokens, so the developer gets to write naturally and no one has to deal with parsing. Just use the json stringified token array as the key in your translation map.

Here's how the tagged template literal maps to tokens:

    t`Where is the ${t.thing()}` ->
    ["Where is the ", ["thing"]] // ["variable name"]
Example rendering a translated string directly:

    t`Where is the ${t.thing(user_data)}?`.toString()
Its internet forum so I made it as short as possible over all other style factors. Untested - just trying to express the idea.

    /** @typedef {[name: string, value?: unknown]} Variable */
    /** @typedef {string | Variable} Token */
    isVariable = Array.isArray
    bind = (token, values) =>
      isVariable(token) ? [token[0], values[token[0]]] : token
    unbind = (token, values) => {
      if (isVariable(token) && token.length > 1) {
        if (values) {
          values[token[0]] = token[1]
        }
        return [token[0]]
      }
      return token
    }
    render = token => (isVariable(token) ? token[1] : token)
    /**
     * Render a translated string:
     * ```
     * t`Some kind of ${t.thing(user_data)}`.toString()
     * ```
     */
    t = (literals, ...args) => {
      // template = ["some kind of ", ""]
      //     args = [t.thing]
      // zip -> ["some kind of ", t.thing, ""]
      const tokens = literals.flatMap((literal, i) =>
        i === 0 ? literal : [args[i - 1], literal],
      )
      return methods(tokens)
    }
    methods = tokens =>
      Object.assign(tokens, {
        bound: values => methods(this.map(token => bind(token, values))),
        unbound: values => methods(this.map(token => unbind(token, values))),
        toKey: values => JSON.stringify(this.unbound(values)),
        toString: () => {
          const values = Object.create(null)
          const translated = TRANSLATION_TABLE[this.toKey(values)]
          const resolved = translated
            ? translated.map(token => bind(token, values))
            : tokens
          return resolved.map(render).join("")
        },
      })

    // Proxy so t.anyKey returns the variable constructor
    t = new Proxy(t, {
      get: (target, name) =>
        Reflect.get(target, prop) ?? ((...args) => [name, ...args]),
    })

    // Example:
    const TRANSLATION_TABLE = {
      // This can be JSON.stringify round tripped fine
      [t`Some kind of ${t.thing()}`.toKey()]: t`${t.thing()} はどこですか`,
    }
    function handleEvent(event) {
      alert(t`Some kind of ${t.thing(event.thing)}`)
    }

    const prepared = t`Avoids ${t.repeated()} JSON.stringify lookups`
    function calledInLoop() {
      console.log(prepared.bound({ repeated: "lots" }).toString())
    }