 Hi everyone, yep, we will be now talking a bit about the tools for internationalization and translation in DHS2 web applications and the tools that we provide to do that. I'm going to go ahead and share my screen. Okay, so let's talk a little bit about translations and then we'll do an exercise on translations and following that we will talk a bit about Datastore and how to use that particularly with the App Service Datastore tool. Before I get into translations, however, I wanted to address the question that was brought up in the Zoom chat at the end of the last session, which is how to access the version of the DHS2 server that you're talking to from a web application. I don't have any slides prepared for that, so I will just go ahead and do a live demo. We'll see how that goes. In order to do that, I am going to actually going to share a different window in a moment. You should see my window now and it should have just gotten a bit smaller so you can actually see it. This is just going to be a way to create a new application from scratch, so we can go through that quickly as well. I'm going to create just a very simple, I'm going to make a new folder called TestApp and open that here. We're not going to get into it. We covered in the previous workshop, we covered how to set up your environment locally and all of that, but in order to do this, I'm going to open my terminal and I'm actually going to go ahead. We are just seeing. I'm going to initialize my application, so I'm going to say D2 Apps Scripts, which I have installed globally already in it. I'm going to say the folder that I just created which is called TestApp and you'll see the files on the left here that are coming in that are being generated by this initialization script. I'm doing this just so we know that this works out of the box in the platform and you can use it for just about anything. While this is loading, I'm actually going to share my entire screen so that I can also show you a browser once we have this up. You should see my screen again. We're downloading this, we're installing the dependencies for this application. Once we have that done, just take one moment. This is taking a little bit longer. I'm going to go ahead and show how this is done anyway. What we're going to use is we're going to use a function called useConfig. This is available from the App Runtime. I'm going to save this file as test.js, just so that I have syntax highlighting. I can import useConfig from hdxs2.app. Now later in my component, whatever the component that I'm building is, anywhere in my application, I can use this hook and I can get some data. I'm not going to fill that out yet. It says useConfig. There's no arguments to useConfig. This will just give you information about the application that you're running in and the server that you're talking to. In the platform, you'll have one of the basic things that you might want to use if you're using another, for instance, if you're using just a plain fetch to get some data from djs2 or something like that, you might want the base URL of the server that you're talking to. You can get the base URL in this way using the base URL return value. You can also get the API version, which will automatically be set to the highest API version available in the djs2 instance that you're talking to. But if you wanted to retrieve that and get that data dynamically, you could use that here from useConfig. There's also a server version, I believe is what this is called. We're going to dump that out here so that we can see what it looks like. I'm actually just going to console log this server version, and then I can say I'm going to return something, whatever it is, return, actually, I'll just say base URL API version, just so we have that rendered on the screen. But what we're really looking for is console logging the server version. So almost done here. This is taking quite slow, probably my internet, but we are now done, perfect. So I'm going to move this test.js into my source folder. I'm going to update my app.js so that I just render instead of all of this. I'm just going to render the test component that I just created. I have to import that. I'm going to go ahead and usually this should be, let me call this test. This is maybe a little bit longer than, longer winded than we needed for this particular example, but we're going to see what this looks like. And this also kind of demonstrates how you can quickly scaffold up an application and test out some different theories. So let's go ahead and run, I have to go into my test app folder and run yarn start. Now I also have this here, so you should still be able to see my screen. Don't let me know if you cannot. And I'm going to go to localhost 3000. So I'm already logged in and you can see that we have rendered the base URL, the app symbol, and the version of the server or the API that we're using. So this is, whenever we construct an API URL, we use these two components. We say the base URL, API, slash version number, and that returns that. But now let's see what was actually logged to our console. So we'll see we got an object here, which has the major version, the minor version, and the patch version for the particular server that we're talking to. So this is a 235.1 server. If we talked to a different instance, if for instance, I logged out here and go back and I'm going to log in now to a different server. This is the same application talking to a different version of the server. We can see that we have a different base URL. Our API version is set to the highest version available for the server, which is 34. And the version that we got back is version two, 35 patch five, and this is actually the snapshot version. So that means that it's not an official release. This is a snapshot of a pre-release of 234.5, because 234.4 is the last latest release. So hopefully that helps a little bit. Let me know if you have any questions, further questions about the use config hook or getting access to server version information in your applications. I'm not looking at the chat. So if Deborah, if anything comes up, if you could just mention it, that'd be good. Thanks. Yeah, sure. Okay, so let's talk a little bit about translations. Sorry, this is the beginning. Where did we go with the translation? So we're talking about translations. As Deborah mentioned, I18N stands for internationalization. There are 18 letters between I and N, which is why it is called that. And underneath the hood, our internationalization system uses something called I18Next. So if I open that up, just to give you a quick introduction, you can use basically all of the functionality of I18Next in DHS2 applications using the D2 I18N library. So there's a lot of functionality here, but this is basically just a way to do internationalization in web applications. So it does that at runtime. That's what it uses when your application is actually running. It uses I18Next to fetch the French version of a particular string in your interface or something like that. But what the platform also provides is a system for monitoring and generating translations. So the CLI app scripts by default. This is how you would do it manually if you wanted to run these steps by yourself. But if you run YarnStart or YarnBuild when you're in a platform application that uses D2 app scripts, all of this will be done automatically. So you don't need to run I18N extract or I18N generate. But underneath the hood, this is what the start and build scripts are doing. So when you run one of those scripts, it will extract all of the strings from your source code and put them into a file called en.pot. That's basically the English version of the strings in this application, which you can then translate to any other language, and it will allow your users to switch between English and French and Swahili, and whatever other language you're using in your application. So what this does is it basically goes through every JavaScript file in the source folder, and it looks for a function calls that look like this one, I18N.t. There's a few different kind of fancier ways to use that in different contexts, but it will always look for I18N.t, and then it needs to be a literal string. This cannot be a variable. This would be a literal string that is what you want to translate, and I'll get into that a little bit more in a minute. So that's what the extract does. It looks through all of your source code files for any places where it uses I18N.t, and then it puts all of the strings that it finds into en.pot. You can then run I18N generate, and again, this is automatically done in the start and build scripts for the platform, but you can run generate, which takes that en.pot file and generates two or more files. One is in source locales. There will be an index.js file that must be imported by your application. So that basically will initialize the translations of all of the components within your application from that generated source file, and it will also generate a number of JSON files within source locales that are basically the English strings, the French strings, the Swahili strings, et cetera. This is going to be improved in the near future, so just be aware of that. Right now, we're generating files into the application itself, but we're hoping to move this into the app shell soon, so that you won't have to deal with it in the application at all. There's a few challenges with making that happen, for instance, loading translation strings from dependencies. Right now, that works quite well, but you need to basically do it at runtime, and you need to have all of the strings loaded in those libraries, but we're going to be working on lazy loading the additional language strings, which can be quite large in libraries as well. Today, in the latest version of the platform, when you load your application and your user is an English user, if you have 10 other languages specified in your application, those language files will not be automatically loaded. That means that it will be a much smaller bundle size for your application. When it first gets loaded, the JavaScript bundle file can be significantly smaller, and then when a user that has their configuration set to Russian logs into your application, it will load the Russian version, the Russian strings that are available, and none of the other languages other than English. It will continue to load the English strings and use those as a fallback, but you will see that the bundle size is much reduced in this configuration. Extract and generate steps happen under the hood for your yarn start and yarn build scripts. It's pretty straightforward to implement this in the platform. This slide doesn't actually tell you that much more information, but basically you just need to add at dhis2-d2-i18n in addition to your CLI app scripts. Probably the CLI app scripts would be a development dependency, but I just put this here for simplicity. Then your start and build scripts are just exactly as they were when you initialized your application in the platform, which is dhis2-appscript-starts and dhis2-appscripts-build. Therefore, automatically out of the box, your application will translate any strings that it finds into other languages. I'm going to go ahead and do a quick demo of this. Actually, first I'll show you the different versions of how to do this. As I mentioned, a simple translation is i18n.t with a literal string, meaning not a variable. That i18n comes from at dhis2-d2-i18n can also be imported equally from the source locales index.js that is generated. I'll show you how to do that. You do need to load that once at least in your app.js. If you are doing something a little bit more complicated where sometimes just simply writing a string like hello world isn't going to be enough, because you might have a variable that the number changes based on some variable in your application, some data that you're loading from dhis2, for instance. Depending on the language that you're translating that string into, the way you translate it changes based on what that number means. In English, for instance, we are one developer or we are x like 10 developers. This example doesn't show how to change developer and developers. You would then use, actually what it does, this bottom example here is for plurals. So you have a count value and then you say it's one like if it's singular and multiple likes if it's plural. Other languages can also have different plural values if they have some languages, for instance, have a different word for one thing, three things, or 10 things. There are different words for life in that language. So that also allows them to specify that in different ways. But at its most basic, we can just substitute a variable into the middle of our string somewhere. In this case, we're putting in the number of developers. This might also be the name of an indicator or something like that. And that can be put into the string using these double bracket notation. I'm going to go ahead and do a quick demo of this. So I'm going to use that same, I'm going to get rid of this provider since we don't need that in the platform app. So now I'm back to basically a very simple application. And what I'm going to do is I'm going to specify a, I'm going to load, first I'm going to import my D2 IATNN, so my IATNN function. So import IATNN. And if you recall, I need to at least once import this from locals slash index.js. You'll note that this doesn't exist yet. So in my source directory, there's no locals index. That can be a bit problematic. This is one of the reasons why we want to move this into the shell rather than in the application itself. But if I go ahead and say H1, I'm going to translate this. So I'm going to say IATNN.t hello world. This could then be translated to any number of different languages. And I'm going to get rid of this test because I don't need it. So now I have, I'm importing my IATNN from locals index. I have this format where I have IATNN.t with a string literal that it will be extracted into the IATNN folder at the top of my application when I run yarn start. You'll notice that I was actually running yarn start when this happened. So my application was running, but it didn't automatically regenerate the translations. In order to do that, you need to run the script again. So you need to either run yarn start or yarn build again. So I'm going to go ahead and run yarn start and keep an eye on the left side of my screen here where you'll see a new directory called IATNN and another new directory called source slash locals that will get generated. So you can see that those, those two got generated here. I'm sorry, it just went too brave. And you can also see in the output of this yarn start script, we say generating internationalization strings and then writing one language strings to IATNN.en.pot. And then it also writes those into this locals directory. So let's look at that en.pot file. There's some information up here at the top. This is pretty specific to what you're looking for in, yeah, what language this is describing and some other things about how to do plurals in for this language. You can look at this basically IATN.en or IATN next documentation will have information about how to do this for different languages. But we'll see here that we have our message ID hello world and then an empty string because this is English, we don't need to specify what it is, but we could easily say hello world exclamation point. Now we can translate that. So I'm going to copy this en.pot file and paste it. So I have copy now, but I'm going to name this one ES. So it's going to be Spanish. And for the P.O. stands for, I forget exactly what it stands for, but P.O.T., the T in that stands for template. So for other languages that are going to be replacing the English strings in our app, we're going to use the .P.O. extension. And so here we're going to keep this the same for now. Again, you could look up how to do this. You can use a service like there's one called trans effects that we use for core applications to generate these files and allow people to contribute their translations, which you can set up for your own applications as well if you'd like. But for now, we're going to do it manually. So our message ID is still the English string hello world, but we need to now translate this into Spanish. So our new translation is going to be hola. So now we have en.pot, ES.P.O. We still only have one translations.json, which is generated when we ran this script. So I'm going to run that again. And you'll see that we also now have an ES. And apparently I changed it in the wrong one. So I made the ES.P.O. hello world and my en.pot hola al mundo. That's going to be incorrect. So I'm going to change that back. I'm going to regenerate this. Now, when we look at our localhost 3000, we see hello world. But if I change my locale, I'm going to change the locale for this particular user. So I go to settings, kind of change the interface language to Spanish. And then I go back to this application. And I see hola al mundo. So this is how we do translations in our application. In the source code, if we look at the source code again, the source code only has the English string. So it just says hello world. But our application knows how to translate hello world into hola al mundo at runtime once it figures out what language the user wants to use. A few gotchas here. This needs to happen. This i18n.t hello world needs to happen at runtime. And that's because we need to translate this after the user is logged in and the application is loaded. Because before that, we don't know what language the user has selected. When the application first loads, we haven't loaded the preferences for the user. And so we have to assume that it's English. In some cases, it can be problematic if you do something like this. Hello text equals this. Hello text. This is problematic because the hello text is translated on initial startup, which is actually in in English rather than in whatever the locale of the user is. So I would recommend avoiding this. If you are using the platform, this actually is probably okay because the application code gets loaded after the initialization of the application. So we actually code split the bundle for a platform app between the shell and the application. The shell loads the server version. It loads the information about the user and what locale they selected. And then it loads the code for your application itself. So it can actually work in platform applications, but just be aware of this that it can be problematic in some cases. So I would recommend generally avoiding it and just keeping the i18n.t function calls in your render functions for your components. The other thing that you want to avoid here is using a variable, as I mentioned. So hello text equals this. If I say hello text here, when I try to generate the strings from all of the source files in my application, which happens automatically on yarn start and yarn build, it doesn't know what this is. So this variable is specified at runtime. So when we're just building this, we don't know what hello text is. This might be coming from other variables. It might be specified somewhere else entirely. So you need to always have the literal string. So the thing that's in quotes needs to be the argument to this i18n.t. Otherwise, it won't be able to generate a translation key. It's another thing to keep in mind. We're going to now add an interpolation here. So we're going to have an adjective. I'm just going to call it adjective. Hello, adjective world. And then we're going to change what that word is based on, let's see. Yeah, let's do that. That's adjective. So if we look at our we'll see that in order to do interpolations, we have number of devs as the variable name in the string and also number of devs as the key in the object as the second argument i18n.t. So I'm going to go back over to the code. I'm going to do that. So I said adjective was the name here. I'm going to call it adjective. And I'm going to say the adjective is going to be beautiful. So now I'm going to go ahead and regenerate this. So now we can see that it says hello, beautiful world, even though the string that we're passing to the translation is hello adjective world. Obviously, this is a little bit problematic because beautiful is in English. And so we would probably want hello, beautiful world to be specified in the translation string. But this is just an example of how to do this. And it's even more useful. So let's say if we go, if we use our name instead, so I'm going to use a data query here equals a call query, source me, fields, or perhaps, sorry. And I need a name for the response. So me, programs, fields, display. And I'm just going to do something like this. I'm actually going to change this. I'm coding quickly here just to show you kind of how to build up an application. Some of this is obviously review. But we have loading error data coming from use data query with this query. And then we're going to end up determining this. And so I'm going to say hello name instead. So this is a good example of a use of the variables or in transition in translations or internationalization. And I'm going to use the for the name variable here, I'm going to pass data. And actually, this is a kind of a fancy trick, but you can actually do question mark dot question mark dot me dot display name. So this is actually going to be a little bit funky because when data is undefined, this will be undefined. So we're going to say hello undefined until while we're loading, and then eventually we'll replace it with the display name of the user. And I think this is actually name that display name, apologies. And we could also as we've seen in the past say if loading return loading, or we could do that within this as well, but not going to do that today. And so I'm just going to do this here and see what happens and get rid of the loading error. And let's see what's going on here. And look, we've got hello, the name. So the hello and the exclamation point both come from the translation. The name comes from the variable that we're passing in that we don't know at compile time. There's no way we could know the name of the user when we're actually loading this. But now we change the translation. So if we go ahead and generate this again, we'll see that this becomes hello, name, exclamation point. But we actually need to change yes as well, because hello world is no longer a string that we want to translate. So we can just copy this over here. And we can say, hello, exclamation point. And actually with this, I'm going to do this here as well, because in Spanish, when you have a an exclamation sentence, you start it with an upside down exclamation point as well. So you would put an upside exclamation point in this translation. Now if we restart this to regenerate the translations, oops, we get this, hello, this is translated to Spanish. Again, if I switch my user back to English, you'll notice also actually before I did that, you'll notice also that all of the header bar, the application names, the search for the applications, the dialogue here is all in Spanish now as well, because I'm configured to be in Spanish. And that's something that you would need to do yourself if you weren't using the platform. So that's done for you as well. I'm going to go ahead and configure this and switch to English again. Now we'll see that hello, is what gets loaded. There we go. So that was a quick demo of internationalization, some things that we can do. I'm not going to go through plurals in the demonstration here today, but you can also do a number of more advanced things that you can find on the IT Next website that allow you to do things like plural handling. Let's go ahead and go through this a little bit. There's a couple things to remember. These are basically what I've already said, but I'm going to go through it again. One that I didn't cover is that you want to make sure that you only have one dhs2-d2-i18n in your project. This means that if you have a library that you're depending on, which also requires dhs2-i18n, you want to make sure that you don't have two different versions in your bundle, because then you'll have two different copies of the translations, and one of them will have the ones that you need, and one of them won't, and you'll end up with missing translations in your interface. So you can use like source map explorer or just looking at the yarn output to figure out if you have multiples of these. That's probably something that we should write up a guide for how to investigate the duplicate dependencies in your bundles, which you should generally avoid. As I mentioned, i18n.t should happen at render time, so you should use that i18n.t in the component render function itself. You need to generate the files that are at source locales, but they should not be committed to your repository as a general rule. So source locales index and source locales language translations that JSON typically are excluded in the git ignore. You should ensure that you only have one version of dependencies which have translations, so similarly to having dhs2-d2-i18n only appear once in your bundle. If you have a dependency like ui-widgets or another library that has translations in it, if you have two copies of that library, you might end up with them overriding each other in the translation map, and so you want to make sure that you don't duplicate that. And then finally, you need to import the source locales index.js somewhere in the application before all the other translations load. Usually this is in source app.js, but in some cases you can just, you can always, I usually try to recommend that you always import from locales index.js, you know, not where you are in your application, so that you never are going to run into a situation where you haven't yet imported this when you're trying to do a translation. Again, that's something that's going to be improved in the near future in the platform, so that it does all of this in the shell rather than relying on doing it in the application. Okay, that's all I had for translations. Does anyone have any questions?