YarnSpinnerTool / YarnSpinner

Yarn Spinner is a tool for building interactive dialogue in games!
https://yarnspinner.dev
MIT License
2.3k stars 201 forks source link

Add substitution format specifier #335

Closed salyu9 closed 1 year ago

salyu9 commented 1 year ago

This is a feature I've currently used in my own project and I wonder if it is appropriate to open this PR. Also more details need to be discussed to make it complete.

This feature add a formatter (or format specifier) syntax to substitutions in lines like many programming languages' built-in libraries do:

Number: {86:0000}.   // will be "Number: 0086." with proper format function.

I added a substitutionFormatterMode to the lexer and let StringTableGeneratorVisitor copied text from that mode to the generated string. And leave that as it is to be handled by the format function. Besides that, a properly object[] RawSubstitutions is added to Line class to be used in format function.

Motivation

Problems I'm not sure my approach to implement this feature is good practice and may need some suggestions.

Other information There's an antlr.sh script that download antlr-4.7, but the previous C# files are generated by antlr-4.9 which may be the bundled antlr version of vscode plugin. I used the latter to generate C# files. Also there are some test-cases fails on Windows due to the newline difference which is used by StringBuilder, maybe the test code need some fix.

salyu9 commented 1 year ago

I tried to make a backward compatible format function that keeps placeholders untouched if failed to substitude (index out of range or invalid format specifier), but it's a bit complex and I doubt the necessity of keeping the compatibility. It will be easy to just return the source text if string.Format(localizedText, rawSubstitutions) throws exception. It is a really small breaking change and users that want more customizations will likely to use their own format functions (like SmartFormat or Unity.Localization).

McJones commented 1 year ago

Hey-oh,

So this is cool, I guess my first concern however is we haven't actually defined how interpolated text is to be displayed so without essentially base line of how things are to be shown providing formatting control is gonna be undefined. That's not a deal breaker, more "oh I guess we should document how things should show by default".

That aside I do wonder though if this is not done better via markup instead of overloading the interpolation syntax. The markup already allows a decent chunk of this, especially around plurals case. I do however think there is a need for some markup that allows numeric formatting, or maybe even a generic formatter markup that passes a format string down into something like SmartFormat.

I guess in the end I wonder what does this give over a new replacement markup that passes the format string to another system instead? Not saying that this is a deal-breaker just curious as to the best way to make this work.

salyu9 commented 1 year ago

Thanks for replying!

I'm sorry that it seems I made the motivation explaning too heavily on the plural examples. The markup works really perfectly for plurals and other situations, and I'm not trying to make the format specifier to replace it. Actually I don't think YarnSpinner should support the plural format specifier by default as another way to do localizations besides the markup approach, that will make newcoming users confused.

What I want to achieve is adding a format specifier syntax support, which may just have basic function to format numbers with implementation-defined behaviour (most likely to be implemented as string.Format in YarnSpinner-Unity). The format handler can then be made customizable for advanced users so that they can do something with the format specifier, such as embed YarnSpinner to their games with the same plural syntax, if they are already using format specifier approach. It's a small syntax supporting that can enable more customizations.

I think a good way to do it might be like this:

One more thing to worry about is whether to just use string.Format. The current search&replace implementation will make translation like {0} and {1} be 123 and {1} if the subsitutions contains only one item 123. If replaced by "try string.Format and catch" it will be simple and easy to maintain, but will be a small breaking change as I mentioned above, the result will be {0} and {1} with 123 not substitude.

The following is not important. My usage is rare and a bit tricky that make using markup more difficult than normal. Maybe it's so rare that it's not worth mentioned but if you are curious about: I'm making a card game that often need to describe a card function like Draw {$count} card(s). The count need to be highlighted in a different color, so after all process it should be look like Draw <color=blue>1</color> card / Draw <color=blue>2</color> cards (upgraded version of this card). Things get worse because other things can buff this card to let it draw more cards, in this situation the count need to be highlighted using a different color like green. I made my custom formatter to do the colorization and plural handling at the same time when substitude, which made the format string as simple as above like Draw {$count:plural one=...} ($count is not a simple number but a value-baseValue pair that will be colorize by the format handler). If I use the built-in markup, the text after substitution will be Draw [plural value="<color=green>2</color>" one=...] which is invalid. There's a way to do it via custom markups like Draw [plural-colorized value="{$count}" base="{$baseCount}" one=...], but I'll need to make a lot change to existing translations and it's also a little confusing or complex to translators. I firstly tried to use the markup syntax for this situation but gave up and turned to use my previous implementation, and made the dialogue part of the game to use similar plural syntax.

st-pasha commented 1 year ago

It's a bit unclear whether number formatting needs to be part of the core language syntax. It doesn't seem like this would be a commonly-used feature, and there are alternative ways to implement the same:

Then you could write something like

Number: {format("%04d", 86)}

to produce a line Number: 0086.

McJones commented 1 year ago

Closing this in favour of #348, thanks for the PR.

By the way we don't often see people making changes directly to the grammar and you did an excellent job with those changes, so while we aren't accepting this PR it is an absolutely amazing one and look forward to future PRs from you.