Search Unity

  1. If you have experience with import & exporting custom (.unitypackage) packages, please help complete a survey (open until May 15, 2024).
    Dismiss Notice
  2. Unity 6 Preview is now available. To find out what's new, have a look at our Unity 6 Preview blog post.
    Dismiss Notice

Question How to properly handle dynamically downloaded translation content

Discussion in 'Localization Tools' started by Aurelinator, Jul 15, 2022.

  1. Aurelinator

    Aurelinator

    Joined:
    Feb 27, 2012
    Posts:
    19
    Hi there,

    I've been working on integrating the loc package into our project. We've got a live game with lots of users, but we've been wanting to add localization for a long time. But because we're live, and we ship new features weekly, it is important for us to be able to have the string content stored server-side rather than client side, since the translation turn-around would cause us to mess up our ship cadence.

    Anyway, because of our constraints, and because a decent amount of string content is already stored on the server, we've made the decision to build our own strings service using a custom CMS that we've built. We've decided that the "Unity Content" on the game client should just be another set of data in this String Service, and that the dynamic server content should just be served down already localized.

    As a result, our design paradigm is such that we have a bunch of client side strings in UI, in code, in assets, etc. that will eventually make its way into various String Tables. We want to keep the English text baked into the client, as that is what our developers and designers are writing... BUT, we want to keep the extra locale tables empty, until we grab data from our server.

    For the sake of simplicity, let's say we're shipping English/French/German.

    This means that our game will ship with English strings, but empty French and German tables. (For the most part - we may bake our login UI into the client since our strings service is authenticated and needs a logged in user to get data). What we want, is... at some point in our login flow, to go to the server and grab the current snapshot of data from our strings service for the locale the user wants, and place it into the string table. Our CMS has a unique version hash so that we can identify if something has changed on the strings side, if we need to do that.

    What I'm curious about is... what would be the recommended method to do this? I can already think of a couple of solutions for how this should work, but have no idea which would be the cleanest solution. Here's what I'm thinking:

    1. The Simple, But Bad Solution
    During my login flow, let's say a user has their locale set to the French, I go hit my strings service for french strings. I get a data object or a stream back, load the empty French table, and start filling it in with the proper translations.

    Clearly, this solution is easy to do. I can load the table, and I can slap data onto the table, but this is clearly not ideal. This also has many really big downsides:
    a) Not Editor Friendly for previewing data, and
    b) Data is not cached anywhere - every launch of the game, I have to download the strings data again, even if the content hasn't changed.
    c) Bypasses any of the loading/unloading stuff the package provides

    2. A Custom StringDatabase
    Better than just slapping data onto the table at runtime, there is the option of creating a custom StringDatabase class so that I can inject the downloading then filling the table in the way that the loc package seems to have wanted. We'd basically just do the same thing as step #1, but basically, instead of using Addressables to load the "empty table" from disk, we'd create one dynamically at runtime and fill it with the server data.

    This is clearly a better designed system, but doesn't seem to solve many of the problems. By bypassing addressables, we lose out on automatic caching, we lose out on editor friendliness, etc.

    3. A Custom Addressables Provider
    Addressables clearly ships with the ability for us to connect to the server and download the data from the server, but none of their custom provider logic is documented and never seems to work the way I expect. For instance, if I've got a json blob that contains all of my strings, it is unclear to me how to convert that json blob into the asset that localization/addressables is expecting it to be.

    Ideally, it would be great to let the Loc System just do its thing... go to addressables, which should provide the data the way it wants. But the default way that everything gets grouped together is that all of the tables of a single language are in the same group. Does that mean they all get placed in the same bundle?

    For example, let's say I have three string tables: UI, Subtitles, Tutorial. I may not want to load Tutorial data unless my users are going to the tutorial area. As a result, how would I write a provider to hit the server with an authenticated request, and convert the data to the bundle format? Our strings service can easily support sending down a subset of the data (one table at a time), but if all of the tables are placed into the same bundle file, doesn't that mean that Localization is loading the entire file into memory then only grabbing what it wants from there?

    My knowledge of addressables is a bit weak here, I admit.

    But here's the question: if I write a custom addressables provider, say a variant of the JsonAssetProvider, will addressables handle caching the data for me? As far as it is concerned, it downloaded a bundle of data. I am able to tell if the server content has recently changed (we store a hash of the content, and if that hash is different from what the server says is live, we should refresh). How would I invalidate that on the addressables side?

    Another option is to download the server content to a local "cached file" then, have addressables try to load the table from a cached file. This might be possible, but our game runs on multiple platforms (Xbox One/Xbox Series, PS4, PS5, iOS, Android, Quest, Oculus, PC) and each platform has slightly different caching expectations about what data is allowed to be stored. So we had hoped to just let addressables cache with whatever it does to store the data...

    So... if I have a table, in french, called "Subtitles", would I be able to have a unique authenticated call to mystringservice.com/strings/subtitles?locale=fr with a token we obtain after logging in? Or would I somehow have to parse the entire french group, and then create individual tables?

    How do I use our custom CMS's idea of a hash with addressables's idea of ContentCatalog hash? Should I have a custom IResourceLocation where my "hash" method returns what our servers idea of a hash is?

    This is somewhat confusing to me. Perhaps I am not explaining myself well or that my understanding of addressables is very weak (it is), but it seems to me that leveraging a custom provider SHOULD be the correct way to do it, but it is unclear how to do this given the fact that they don't have any documentation on how this should be done... any advice here would be well appreciated. We currently use addressables, but all of our groups are just bundled in the game at the moment so it is unclear how a server-side downloading of individual items should work.

    4. Editor Previewing Solution
    Even if we end up getting Addressables to load the data in, this doesn't change the fact that, in editor we lose out on a bunch of functionality by not having our translations baked in, and only having the english text. We can test things use pseudo-locales, but sometimes it would be great to see the other languages while constructing a UI prefab, for instance.

    I have written some code that lets me authenticate to our server and download the data at editor-time without a user-token, but we don't want our developers to check in the translations accidentally. Is there a clean way for us to be able to have a preview of the real data without populating the tables with real stuff at editor time? Can we mark the tables with some kind of hideflags where we change their content but don't want anything actually saved to disk? Should I have a separate EditorStringDatabase that creates "in-memory" string tables as opposed to writing it to the ones that are saved in my project? Basically, how in the world can we get real data in there, so that switching languages shows us the real translations, but not actually get our devs to see this real data in their diff when they're about to commit a feature?

    Conclusion
    Thank you for taking the time to read/respond to this. This is fundamentally a design question about "best-practices", because we're doing something a bit strange. However, I'm sure I am not the only person working on a game where the content will be stored server-side, and not in the bundle format, and downloaded dynamically.

    I'm trying to create both the cleanest solution but one that doesn't take a month to implement and constant management after the fact, so I appreciate any advice you may have on this topic! Thanks!
     
  2. karl_jones

    karl_jones

    Unity Technologies

    Joined:
    May 5, 2015
    Posts:
    8,344
    Thats a lot to take in ;)
    Im afraid I don't know the answers to most of the Addressables questions, I asked the team if they can take a look at the post.

    As for updating the tables at runtime there are a few ways I can think of:

    1) This example shows patching a table that has been loaded. Its similar to your number 1. It would only work in playmode and you would need to find a way to cache the files from your server. So its not ideal https://gist.github.com/karljj1/f2707f9eee7f4cff37fe90493d9098c4 that
    You could try caching using https://docs.unity3d.com/ScriptReference/Application-persistentDataPath.html

    2) You can do something similiar to 1 but control the creation of the tables. This would let you ensure that the table is updated before its used but would also require playmode and you still need to find a way to cache the downloaded data. https://forum.unity.com/threads/creating-stringtable-at-runtime.999439/

    3) We have a new feature coming in 1.4 called ITableProvider and ITablePostProcessor. This lets you create a custom table and also patch an existing table before it is used. This works both in the Editor and play mode however you would still need to handle the caching of the server data. 1.4 is scheduled for an October release.

    4) Updating the Addressable assets after a build, remote content distribution. This is the ideal way addressables likes to do things. You distribute the game in English and host the other language asset bundles on a server. Addressables can then pull the other languages when needed and you could update them on the server at any time. Addressables handles the caching so it doesn't need to happen every. time. https://docs.unity3d.com/Packages/com.unity.addressables@1.19/manual/RemoteContentDistribution.html
    This would not work during Edit mode though as we read the asset files directly however you could keep the files updated in the Editor, when a build occurs it would only bundle the English tables as the other ones would be uploaded to a server for hosting.
    https://docs.unity3d.com/Packages/com.unity.addressables@1.19/manual/ContentUpdateWorkflow.html
    You can also automate the addressables groups that are used by new tables/assets https://docs.unity3d.com/Packages/c...ual/Addressables.html#addressable-group-rules

    I think for your use case 4 may be ideal however it would mean that you need a step that converts the CMS data into addressables on the server.
     
    Last edited: Jul 15, 2022
  3. andymilsom

    andymilsom

    Unity Technologies

    Joined:
    Mar 2, 2016
    Posts:
    294
    For the Addressables side, it mainly handles file content, particularly build AssetBundles from Editor.
    I would be careful with just having english and downloading others for some platforms. Addressables caches where the platform has caching enabled, but other will just download and not cache.

    For downloading text data using custom methods from a string service. You could technically use Addressables, but it doesn't seem that useful to you honestly, you may as well just write some downloading code outside of the system, which puts the data into the localization system directly.

    TextDataProvider will download a text file and return the text (Does not cache the file). You could use this by making an ResourceLocation that has the type of TextDataProvider for its provider. JsonProvider also can be used this way, which will do a JsonUtility.FromJson to create the type requested and deserialise the json from the text downloaded from TextDataProvider.

    The same goes for any custom provider. You can make an IResourceLocation pointing to the provider and have data in it that the provider uses. Adding it to an active Addressable.ResourceLocator is needed to be loaded through the Addressables API by finding it by a key. Or simply pass it to Addressables with Addressables.LoadAssetAsync( IResourceLocation ).
    A common way to do this, would be to have a custom provider that has a dependency on the TextDataProvider, so you can get the result of that (text) from the dependency. Note that you still need to do caching and check for caching before yourself, if doing it this way.
     
    karl_jones likes this.