Support both named and positional arguments
It's almost possible with something like that:
sprintf(sprintf('Test with %s mixed %(value)s', 2), { value: 'positions' })
Doesn't look pretty, but at least it could work without much efforts. But it won't, because after first function will finish processing, it already will throw that you can't mix positions types for now. And even if it wouldn't throw it, it will return 'Test with 2 mixed ', because sprintf processing whole input, and there will be nothing to process for second function.
It could be much easier, if it had way to say to sprintf that missed placeholders should be returned as they are.
Let's say, like this:
sprintfp('Test with %s mixed %(value)s', 2) # -> 'Test with 2 mixed %(value)s'
# `p` might stand here for `preserve` type of `sprintf`
Btw, it would be useful not only for mixed named and positional arguments, but for cases when you need to get string with unmatched placeholder "as it is".
Also, it's possible algorithmically to decide whether mixed positions should kick in or no.
For example, when we have
sprintf('Test with %s mixed %(value)s', 2, { value: 'positions' } ) # -> 'Test with 2 mixed position'
it isn't hard to detect, that we have kick-in mixed replacement here, since we can check that we have only two arguments, one of which {number|string|array) and another one {object}. It isn't important how they are ordered, but their types — by checking their type it becomes obvious that {number|string|array) is for positional placeholders, while {object} — for named.
And for some I guess chaining would be ideal option:
sprintf('Test with %s mixed %(value)s', 2).named({ value: 'positions' }) # -> 'Test with 2 mixed position
sprintf('Test with %s mixed %(value)s', { value: 'positions' }).positional(2) # -> 'Test with 2 mixed position
sprintf('Test with %s mixed %(value)s').named({ value: 'positions' }).positional(2) # -> 'Test with 2 mixed position