Hacker News new | ask | show | jobs
by somat 237 days ago
So that's why template literals are broken. I am not much of a JS dev but sometimes I play one on TV. and I was cursing up a storm because I could not get templates to work the way I wanted them to. And I quote "What do you mean template strings are not strings? What idiot designed this."

If curious I had a bright idea for a string translation library, yes, I know there are plenty of great internationalization libraries, but I wanted to try it out. the idea was to just write normalish template strings so the code reads well, then the translation engine would lookup the template string in the language table and replace it with the translated template string, this new template string is the one that would be filled. But I could not get it to work. I finally gave up and had to implement "the template system we have at home" from scratch just to get anything working.

To the designers of JS template literals, I apologize, you were blocking an attack vector that never crossed my mind. It was the same thing the first time I had to do the cors dance. I thought it was just about the stupidest thing I had ever seen. "This protects nothing, it only works when the client(the part you have no control over) decides to do it" The idea that you need protection after you have deliberately injected unknown malicious code(ads) into your web app took me several days of hard thought to understand.

4 comments

I've written a fair number of custom template literals, and I don't understand what your complaint is. Can you share more details?
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.
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())
    }
Yes the CORS threat model was also reversed for me. Couldn't understand it. Eventually I got it...
This has nothing to do with xss or security. Its also a pretty common for template literals/string interopolation to work like this. There are a couple of exceptions, but the majority of programming languages do it this way.

Its why they are called "literals".

As far as I can tell JS has no way to symbolicly handle unformatted templates and then format them later.

For example, you can't do this.

  const t1 = new Template('Hello ${name}');

  const str_1 = t1.format({'name':user_name});
You could argue, perhaps correctly, that this is by design and doing something like this is a mistake. But when my whole clever idea depended on doing exactly this, I was a bit surprised when it does not work with native templates.
Sure. And you can't do it in php either.

I'm not saying its right or wrong just that php is following the trend with this feature when it comes to language design.

I know i said earlier its not for security, but it could very well be for security (not xss though) as format string injection is a common vulnerability in c and python which allow this sort of thing.

What do you mean "broken"? Template literals are great.