#Localization Settings asset does not
1 messages ยท Page 1 of 1 (latest)
Go to Localization Settings, String Database, Smart Strings, Source. Add a Persistent Variables source if one does not exist. Add the reference to the global variables asset to it.
yeah, no worries
I found how to add global one
I'm currently in a middle of figuring out smth else
I'm doing data binding with UI toolkit (can't wait for Unity to finish it's own at the end of a year) and I'm trying to figure whether it would be possible to manually assign variables to smart strings BEFORE Unity does it.
in other words, I want to obtain {Workers} key in code before Unity throws that it can't resolve such variable
Yeah there are a few ways to do this. How do you fetch the localized string? Are you doing it in code?
yes
Can you share a snippet?
I have reference to Table and string key
this is pretty much all I have regarding LocalizedString, I did it previously through my own code only
but I want to make it even more streamlined for development
Yes you can add a persistent variable for Workers
that's not what I need
I am using MVVM pattern in my UI implementation
and binding object is always known
so what I want
is to bind to Properties of that object (it has INotifyPropertyChanged interface) through keys defined in locale
basically
hmm
so, developer writes such localization string
and then in his binding path he writes key for that string
now, if I simply don't do anything
Smart will automatically try to resolve Workers variable
and fail
without giving me a chance to add that variable
but I guess that won't be possible, according to source of LocalizedString
it simply has no such callbacks
ok I see. Well you could try adding a custom Source. The Sources are executed in the order they are defined in the settings so if they all fail you could be at the bottom of the list to handle it
yeah, but I'm not sure how I can pass anything into Source from outside
since Properties to which variables should be bound to are defined through code
You can access it through the LocalizationSettings. E.G: LocalizationSettings.StringDatabase.SmartFormatter.GetSourceExtension<MySource>()
The Source will be visible in the settings and can have serialized fields or a custom property drawer
How would you prefer it to work?
the perfect scenario
just the way I showed on screenshot
so developper simply writes Property name in braces
and in Binding of specific UI element - just key of that string
I assme
that is possible with custom source
he will just need to add additional selector key
{b:Workers}
I assume ISource will be able to figure b and obtain Workers as argument somehow?
Hmm. So when a selector is missing you want to add it to the LocalizedString?
In that example Workers is a formatter
:
oh, so it needs to be b.Workers?
You can chain selectors so b.Workers
yes
b will be evaluated, then if its found Workers will be evaluated and the value from b will be available to check against
So that would give me true and I will have access to Workers string?
public string selector = "b";
public bool TryEvaluateSelector(ISelectorInfo selectorInfo)
{
if (selectorInfo.SelectorText != selector)
Yes you do have access to Workers however it is better to think of each selector as an independent task. So b is one and Workers is the other.
If you want to handle both then you just return an object that contains some info you need for the next selector, Then it will come back to you
Don't think it's a good idea to create selector for literally each possible variable
You want a single selector for b.Workers?
yeah, user defines which ViewModel (with properties) gets which StringTable in UI Authoring
so I don't want to do the same in tables
or in binding path
kind of like this
binding is done through text e.g. <ui:Button text="#Settings" display-tooltip-when-elided="true" name="Settings" />
So assuming Settings key in MainMenu string table has some variables. I don't want user to define anything else in UXML
and instead define Property names in localized string
ok I see. So "Settings" is the key to an entry in the String Table?
so if for example MainMenuViewModel has property CurrentTime
And localized string for Settings key would be Settings {CurrentTime}
I want it to be all user has to do
And at the moment the LocalizedString is unable to query the MainMenuViewModel properties?
but so far the best I managed:
#Settings:CurrentTime
With localized Settings {0}
I simply don't have access to localized string (even unprocessed) before I get exception about unresolved variable
FormattingException: Error parsing format string: Could not evaluate the selector "Workers" at 10
Workers: {Workers}
----------^
UnityEngine.Localization.SmartFormat.SmartFormatter.FormatError (UnityEngine.Localization.SmartFormat.Core.Parsing.FormatItem errorItem, System.Exception innerException, System.Int32 startIndex, UnityEngine.Localization.SmartFormat.Core.Formatting.FormattingInfo formattingInfo) (at ./Library/PackageCache/com.unity.localization@1.4.2/Runtime/Smart Format/SmartFormatter.cs:347)
hmmm
I got some idea
to test
ok. It does sound like a custom source that accesses MainMenuViewModel would be a good approach. If you want access to the raw string then you can use LocalizationSettings.StringDatabase.GetEntry
Nah, this does not help either
table.TableChanged += OntableChanged;
_string = new LocalizedString(table.TableReference, key);
_string.StringChanged += OnChange;
}
private void OntableChanged(StringTable value)
{
Debug.Log("TableChange");
}
private void OnChange(string value)
{
Debug.Log("StringChange");
}
StringChange happens before TableChange, thus exception will be thrown first
ohh. Ok we have a new feature in 1.4.2 called ITablePostprocessor. That could work.
It is called after loading the table but before anything is done with the table
Thats the part that assigns it. The top example is the runtime part
You can also apply it at runtime. Its just this example shows how to assign it so you never need to assign it again. It will be part of the settings
Yeah, that worked
[Serializable]
public class Kek : ITablePostprocessor
{
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.AfterAssembliesLoaded)]
private static void Register()
{
// Create an instance of the table provider.
var provider = new Kek();
LocalizationSettings.StringDatabase.TablePostprocessor = provider;
}
public void PostprocessTable(LocalizationTable table)
{
Debug.Log("PostProcess");
}
}
it is indeed called before exceptions
๐
so
that called
when language is already chosen
and table is loaded
so I can access localized strings?
that could work
Not all tables may have loaded
Is there meant to be only single TablePostProcessor?
Yes but you can always create a version that is a list of them.
Its just a callback. In hindsight it could have been an event ๐ค
it's more of a thing is that I'm providing a framework here, so
Although it would not be serialzable if it was
premaking something is not really I'm looking for, especially if it's includes tweaking project settings in a ny way
ah yes. Well if you are assigning it at runtime you could always keep track of the old value if one is assigned and call it after your callback has executed
๐ค
any idea when that callback is made then?
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.AfterAssembliesLoaded)]
So I assign as late as possible
so in a very case nothing overrides my instance
The callback could be executed during Start from the LocalizedBehavior which uses ExecuteInEditMode
I think that is after your callback
Ill make a note to support multiple providers in the future so you dont have to worry about overwriting.
yeah, modding would love that
new LocalizedStringTable("Story");
I'm guessing that how I can create any table during any moment after PostProcessor callback happened?
since in Authoring I have LocalizedStringTable
while Postprocessor has LocalizationTable
no direct reference
๐ค
yeah, I'm not sure how to get Entries without tableChanged callback on LocalizedStringTable
nvm, turns out it's already updated by that moment
You want to make permanent changes to the tables during this callback?
no, read only
ah great ๐
what I am trying to do now
is analyzing every entry that is .IsSmart
and getting all braces from it {value}
so I can attach variables
with that value
before Unity throws exceptions at me for unresolved variable
You can parse it using the Smart string system to extract the variables instead of checking for braces
What system?
LocalizationSettings.StringDatabase.SmartFormatter.Parser.ParseFormat
LocalizationSettings.StringDatabase.SmartFormatter.Parser.ParseFormat("TEXT", LocalizationSettings.StringDatabase.SmartFormatter.GetNotEmptyFormatterExtensionNames());
that method looks big
I implented a way simpler one ๐
public static string[] GetFormatKeys(string str)
{
var count = 0;
// -4 is because after opening `{@` there needs to be at least 1 char for key, and 1 char to close braces.
for (var i = 0; i < str.Length - 4; i++)
{
var chr = str[i];
if (chr is '{' && str[i + 1] is '#') count++;
}
if (count > 0)
{
var resultInd = 0;
var result = new string[count];
for (var i = 0; i < str.Length - 4; i++)
{
var chr = str[i];
if (chr is '{' && str[i + 1] is '#')
{
i++;
var substr = str[i..];
var ind = substr.IndexOf('}');
if (ind <= 0)
{
throw new StringParsingException(
$"Invalid format provided: {str}. No enclosing brace was found.");
}
result[resultInd] = substr[..ind];
resultInd++;
i += ind;
}
}
return result;
}
return null;
}
Ah yes you have your own format ๐
oh no. You are loading a table inside of the callback?
yeah
Its going to keep calling into your processor causing the recursion
If its already loaded it wont
how else can I obtain Table then?
If it needs to be loaded it will
ah
wait
if (_tableCollectionName == tableCollectionName)
I do this check
public void PostprocessTable(LocalizationTable table)
{
onTableLoaded?.Invoke(table.TableCollectionName);
_previousValue?.PostprocessTable(table);
}
which is obtained from here
so I obtain table only if it is being postprocessed
Is that causing the recursion?
its something with a string comparison I think
StackOverflowException: The requested operation caused a stack overflow.
System.String.Equals (System.String a, System.String b, System.StringComparison comparisonType) (at <8f06425e63004caf99a79845675f751e>:0)
UnityEngine.Localization.LocaleIdentifier.Equals (UnityEngine.Localization.LocaleIdentifier other) (at ./Library/PackageCache/com.unity.localization@1.4.2/Runtime/Locale.cs:162)
System.Collections.Generic.GenericEqualityComparer`1[T].Equals (T x, T y) (at <8f06425e63004caf99a79845675f751e>:0)
System.ValueTuple`2[T1,T2].Equals (System.ValueTuple`2[T1,T2] other) (at <8f06425e63004caf99a79845675f751e>:0)
System.Collections.Generic.GenericEqualityComparer`1[T].Equals (T x, T y) (at <8f06425e63004caf99a79845675f751e>:0)
System.Collections.Generic.Dictionary`2[TKey,TValue].FindEntry (TKey key) (at <8f06425e63004caf99a79845675f751e>:0)
System.Collections.Generic.Dictionary`2[TKey,TValue].TryGetValue (TKey key, TValue& value) (at <8f06425e63004caf99a79845675f751e>:0)
UnityEngine.Localization.Settings.LocalizedDatabase`2[TTable,TEntry].GetTableAsync (UnityEngine.Localization.Tables.TableReference tableReference, UnityEngine.Localization.Locale locale) (at ./Library/PackageCache/com.unity.localization@1.4.2/Runtime/Settings/Database/LocalizedDatabase.cs:371)
yes
ohhhh
so... Any alternative ways of obtaining entries?
Hmm im confused about that error. I cant see why its causing an overflow.
Is that the full callstack?
Yeah StackOverflow is usually recursion
_previousValue?.PostprocessTable(table);
ah yes
no domain reload
no static reload
_previousValue = LocalizationSettings.StringDatabase.TablePostprocessor;
But seems like this is at Unity's fault as well. Since that's the only way I obtain value
Yeah, this should do I hope
public class TablePostprocessor : ITablePostprocessor
{
private static ITablePostprocessor _previousValue;
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.AfterSceneLoad)]
private static void Register()
{
_previousValue = LocalizationSettings.StringDatabase.TablePostprocessor;
LocalizationSettings.StringDatabase.TablePostprocessor = new TablePostprocessor();
}
public static event TableLoadedEvent onTableLoaded;
public void PostprocessTable(LocalizationTable table)
{
onTableLoaded?.Invoke(table.TableCollectionName);
if (_previousValue is not null && _previousValue != this)
{
_previousValue?.PostprocessTable(table);
}
}
}
๐
You may want to do a check on the type instead of this. if _previousValue is not TablePostprocessor
In case its already been assigned somewhere else
Damn. What can I use so it's not parsed as selector or formatter?
ParsingErrors: The format string has 1 issue:
'0x23': There are trailing operators in the selector
In: "Restrooms: {#Restrooms}"
some prefix
For the #?
๐ค
You could add # as an allowed selector
This will now expect your selectors to start with #
Isnt that what you want? Or you want to do a find and replace to add the correct selector in?
I guess I'll just do it without selectors at all
I kind of wanted to have some prefix to explicitly show that binding is done to ViewModel
not some variable or anything
You could use the custom source. If the selector starts with # then you handle it
But I don't want to define any sources in settings, that's the point. I'll consider it though
yeah, that could be solution, but for now I just want to make it work
It does sound like what you need is a custom source. The source is responsible for converting the selector into an object. Yours would do that through the bindings
yay, it works
Now all authoring is required - just key of entry
<ui:Label text="#Workers" name="Workers"/>
<ui:Label text="#Military" name="Military"/>
<ui:Label text="#Scientists" name="Scientists"/>
<ui:Label text="#Budget" name="Budget"/>
<ui:Label text="#Barracks" name="Barracks"/>
<ui:Label text="#Restrooms" name="Restrooms"/>
<ui:Label text="#Labs" name="Labs"/>
Aaaand it binds to ViewModel
thank you so much, sir
through horrible reflection for now ๐
I'll optimize it for primitives, using explicit generics though, so there will be way less PropertyInfo.GetValue calls
What is GetFormatKeys?
that substring code that gets all keys between braces
@pliant reef sooo, I am back at working on smart string parsing
Do you happen to know whether I can somehow get all variables in a raw string of localization with all selectors and formatters taken into account?
I want to implement nested variables support
and also support of other selectors
You want to parse the string to extract that info?
Yeah, so it looks like this
private void OnTableLoaded(string tableCollectionName)
{
if (_tableCollectionName != tableCollectionName) return;
var table = _table.GetTableAsync().Result;
var entry = table[_ls.TableEntryReference.Key];
if (entry is null || !entry.IsSmart) return;
var format = entry.Value;
I have raw entry
Try LocalizationSettings.StringDatabase.SmartFormatter.Parser.ParseFormat
It will return a Format that contains everything. You then iterate through the Items1
Nested will contain another Format in the Items
๐ค
com.unity.localization\Runtime\Smart Format\SmartFormatter.cs shows the full process of parsing and formatting a string
Start at line 189
var formatParsed = Parser.ParseFormat(format, GetNotEmptyFormatterExtensionNames());
Then line 300
Step through each item
hmmm, I got another trouble. For some reason some tables are not loaded and my hook into postprocessor does not trigger
public LocalizedStringBinding(LocalizedString ls, INotifyPropertyChanged binding)
{
_ls = ls;
_table = new LocalizedStringTable(_ls.TableReference);
_tableCollectionName = ls.TableReference.TableCollectionName;
TablePostprocessor.OnTableLoaded += OnTableLoaded;
}
private void OnTableLoaded(string tableCollectionName)
{
if (_tableCollectionName != tableCollectionName) return;
so, I have stored LocalizedString in other table
and either it's somehow preloaded, that's why there's no postprocessing
or there's something else
The postprocessing is always called, it should never be skipped. Maybe you are not registered at that time?
I do it during approximately Start
Is this in playmode?
yes
tables are definetely not loaded instantly, as I see wrong texts
but there's different thing about those
We also do init in Start so it could be getting missed. It could be a script execution order issue. You could also try in Awake
I do it from within ECS system ๐
oh ๐
but they should not be loaded anyway
because it's async
while my init and binding is instant
The async stuff can be instant at times, especially in the editor when using the fast addressables play mode
fastest will be instant, use existing will be slowest. Im not sure about simulate ๐
I would use the bottom and use the actual data
if its built
Can you attach a breakpoint to com.unity.localization\Runtime\Settings\Database\LocalizedDatabase.cs line 722 in PatchTableContents?
hmm. May be because its a package. One thing you can do is move the package into the project. Copy it from the package cache folder into the project Packages folder. You can then make changes and debug. Once you are done just delete the package and it will switch back to the cache
just some backup ๐
"dependencies": {
"com.unity.localization": "1.4.2"
},
We also have 1.4.3 now with some small fixes ๐
You shouldn't need to change the manifest file
it's my package
ah
I think it should still allow the above approach. If it detects the same version in the project then it should switch to that
I do it all the time when debugging addressables ๐
ahem even with local package it won't be recognized, odd
strange
Check that you are generating a cs proj file for the package
in Preferences/External Tools
ah, right
Maybe also regenerate the project file to be safe
ok, it got hooked
aaand, my tables aren't here
Once again, I don't reference those tables anywhere
in my code
I use LocalizedString directly
in ScriptableObject even
The table is null in the callback?
the correct table is never inside callback
callback goes through all my tables that are referenced
Im not sure I understand. The table is loaded but the callback never fires?
oh so you are not using the table yet?
If its not marked as preload it wont be loaded until its needed
๐
oh wow, looks like everything is pretty much ready to grab. Just gotta figure out how it works now
one thing that I am intesrested is whether there's a way to figure, whether placeholder is already resolved (global variable for example)
or maybe some local variable was created before (but that's not critical)
No thats not possible without formatting and passing in the data. It gets especially complicated when dealing with implicit formatters/source where a different formatter/source may be used based on the data passed in
I see...
E.G
I guess one way is to just ignore everything that uses all global variables keys
at the start of a string
at least this way I won't mess with global variables
Potentially however there could be times when it wont work.
E.G "My {global.value}". if You happen to pass in a class instance with a field called "global" and the reflection source is before the persistent variables source then it would use the class instead
well yeah, but I don't think that would be the case
or at least
there will be a warning about such niche cases
on the other hand
I can just parse everything
Yeah it really depends on the project. If you know its safe then you are fine
and if there is variable called g then why not parse it for target property?
but annoying part - that makes parsing heavy
and loading longer
Yes its tricky to do in a generic way without the source data.
Its even possible to change global variables at runtime, such as different ones for each scene. So you may not have a true picture of what is a global variable
so...
if I have a key like this {Settings.LabCost}
Is there a way I can make a nested dictionary of string,IVariable or smth similiar to bind it to LocalizedString?
You can add a LocalizedString as a persistent variable
Can you control what data is passed into the format?
Here's generally what I do
var getter = DelegateUtility.Get(property, target);
var stringVariable = new StringVariable { Value = getter().ToString() };
var name = property.Name;
var action = new PropertyChangedEventHandler((_, args) =>
{
if (args.PropertyName != name) return;
stringVariable.Value = getter().ToString();
});
var targetObject = (INotifyPropertyChanged)target;
targetObject.PropertyChanged += action;
_bindings.Add((targetObject, action));
_ls.Add(key, stringVariable);
You can use a dictionary although im not sure it will work with LocalizedString
that works on non-nested keys
Maybe try creating a Nested Variables Group
So... simple nesting through . works fine, but it appears that parser does not split more complex nested variables
{NestedFloat} Nested bool = {NestedBool}
original Nested float = {Group:{NestedFloat} Nested bool = {NestedBool}}
how does built in parser work with that?
๐ค
Maybe I shouldn't create variables
and instead I could attach custom source
to bound LocalizedStrings
that certainly seems like a cleaner approach
I didn't really liked the approach at first, as I didn't use LocalizedString atm
nah, that won't be good for perfomance during runtime, I should parse string myself then.
But I am yet trying to figure out how to properly parse whole string, as so far Parse method doesn't do recursive
Sorry I missed your messages. Are you still having issues with nesting?
Kind of. I figured how it works generally and found an interesting way:
I'll add argument to LocalizedString, which will be detected by ISource
And I'll subscribe to both: table postprocess event and string changed event. This way I'll be able to know whether bindings are dirty or not.
Now I don't have to parse everything myself and my own parser will simply be fed with already parsed hierarchy.
So basically, for now I'm just figuring how to properly generate variables the way nesting will be supported.
public class BindingGroup : IVariableGroup, IVariable current attempt - my own group with binding logic.
Sounds interesting. If you have any feedback on how things can be improved I'm happy to hear it ๐
actually I have questions instead:
do you happen to know if there is a universal way to test whether Selector is nested withing {nestedGroup:{nestedVariable}} or whithing {nestedGroup.nestedvariable}?
as far as I understand, there are only 2 splitters: : and .
. is a splitter for sources and : if for providing the formatter. https://docs.unity3d.com/Packages/com.unity.localization@1.4/manual/images/SmartString-Structure.dot.svg
here example. current selector is a group. But I'm not sure how to figure whether it's one
Where do you need to figure this out? In a custom source?
yeah
๐ค
maybe I don't need to figure it
and just parse it as a variable of type group
nvm
won't be possible without knowing it
I think you can find out if you are currently nested by checking if the parent is null
hmm
Yes the FormattingInfo Parent field should tell you
but here's the thing
how can I know that I don't need to parse it as variable?
This is kind of non-trivial as I initially thought
I thought you were using # to indicate thats it needs handling? Does that not work?
nah, I didn't do that
I just try to add variables, so that next group can parse them
Persistent one
allthough
maybe I will need some indicator for binding
I think an indicator would make it easier to parse
That depends on the configuration. It can be part of a selector
You can add it to the AllowedSelectorChars
In the settings
yeah, I think that's the way. At least that would make development much easier and straightforward
Nested float = {#Group:{>NestedFloat} Deep bool = {#Deep:{>NestedBool} Deep float = {>NestedFloat}}}
For some reason this cannot be parsed.
ParsingErrors: The format string has 1 issue:
Format string is missing a closing brace
and I believe this could be bug
huh
if I add a space before last brace it works
aside from that everything seems to work
Yes this is by design. By default it uses the String.Format rules where }} and {{ is used to represent a literal } and {. You can enable the Alternative Escaping option in the Smart Format settings so that it always requires a \ before a literal, so \{ and \}. You will then be able to do {{ and }}.
oh, huh
wait, so if I use }}}}} that would work then?
I should check
no, it doesn't. Is there any white space symbols in Localization?
What do you mean?
}}}}} will become 2 literal } and a placeholder end
E.G Hello {{{{{variable}}}}} would become Hello {{123}} if variable = 123
Just wondering whether there is a way to not have extra space in the end without alternate ending
allthough not really an issue
btw, I did pre-release it and I think package is ready for usage
https://github.com/bustedbunny/com.bustedbunny.mvvmtoolkit
Model-View-ViewModel Toolkit for using with Unity UIToolkit. - GitHub - bustedbunny/com.bustedbunny.mvvmtoolkit: Model-View-ViewModel Toolkit for using with Unity UIToolkit.
oh nice! Hmm im not sure how to avoid the whitespace issue. Maybe theres some unicode character that can be inserted that wont be visible?
When you bind in the Uxml how do you know what table the Key belongs to?
This is part of authoring process.
In order to create UI, user needs to premake a hierarchy with Views and ViewModels.
Views know their tables
here my own game as example
ah very nice ๐
Hmm. I have encountered very odd behavior.
Is locale switch supposed to Refresh all strings?
This causes errors, due to the fact my arguments are assigned to specific LocalizedStrings only, while language switch refreshes all of them without any reference to my argument (binding)
@pliant reef hello sir once again.
I encountered another challenge: localized asset binding.
The problem - I can't find a way to determine table's asset type with only having LocalizedAssetTable reference.
Do you happen to know whether it's even possible to determine table type?
all right, seems like Addressables.LoadAssetAsync<Object> works just fine and provides me type. Allthough it's not clear how can I get this type during binding yet, but I have some ideas