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())
}