 is often said that the world is a big place and yet the Internet has this uncanny ability to make it a really, really small place. Every day we might be conversing with users anywhere in the world. They might be your next door neighbour. They could literally be half a world away. Often you wouldn't even know. From a needle-touristic point of view, it's nice to say, oh yeah, we should make our applications usable by as many people as possible, o'r ffordd o'r gallu i'r Unig, o'r eu Hwrth or a'r Chynll, oherwydd, yn ymgyrchol yn ddod, oherwydd yw mynd i'w bod yn ymgyrchol. A ydych chi'n gweithio. Felly, yn rhaid i chi, rydw i'n rhaid i'w ddweud y ddechrau'r ffaith a ffwylltio. Yrhaid i chi, rwy'n arnyn nhw'n 5th i'r unig. Wrth gwrth y gallwch chi'n ymgyrchol, roedd yma'r cyfnodd yn ymgyrchol. Joe, Sam, the volunteers, the AV crews, and this conference wouldn't be what it is without those people. So, from myself, thanks to those guys. So, my name's Liam Wiltshire, says there, I'm a senior PHP developer and business manager. So, hands up, who hates salespeople? Yeah, I get to hate myself. Because business manager is basically glorified bringing people and trying to sell them enterprise. But there we go, it's fine. And I work for this company called Tebex. Now, you've probably not heard of us, we're a fairly small company. But effectively, we run a SaaS G-commerce platform for sandbox games, such as that one, it's Minecraft. Which is awesome because my kids think it's great because I get to speak to these YouTubers and stuff and I have no idea who they are. And yeah, it's like, oh, you spoke to this guy, it's like, yeah, he's a customer. But they think it's awesome, so I've been cool. Last 18 months, I've been cool for the first time in my life. Now, that would probably make a good company logo on a global express shipping or something. But actually, as I said, the world is a big place. But all the while, internet access is growing worldwide. In all corners of the globe, not that globes have corners, there are massively expanding markets that are opening up and they are opportunities for anyone who is selling a service or a product, or whatever that might be, on the internet. Now, most of these do not use English as their mother tongue. Yes, you could say, well, a fair number of people probably do speak English. Firstly, that is a horrible assumption to make. Secondly, it's not true. Yes, of course there are English speakers, same as, well, I speak a tiny bit of French. And a lot of French people speak far better English than I do French, that's just a fact. But there are also plenty of people that, don't speak English hardly at all, and certainly if you're talking something technical, it wouldn't be accessible to those people. So you're missing the opportunity. I've pulled up some statistics. Now, these are to do with the digital gaming industry because that's what we do. But you can find very similar numbers for anything. Now, the global digital games market is now worth over $100 billion a year. That's an obscene amount of money. A lot of that is mobile gaming. Now, Minecraft has a mobile gaming version, and we take advantage of that as well. And previously, it would have been the case that most digital spend was in Europe and North America. These days, it's not even half. You know, China spends more on digital gaming than the USA. So why would you not want to take advantage of that market? You know, and equally in China, as it says there, secondary sales, so that's not buying the game itself, that's buying power-ups or add-ons or extra lives or the sorts of things that we sell, counts for 88% of their market. So of that, over half of the global market they have, 88% of that is secondary sales. So that makes a lot of sense. So before we go on, obviously the title of the talk has a letter I and a number 18 and a letter N in it. So let's talk about what that means. Internationalisation. Now, this is a quote that I stole from, I think Wikipedia, should have put the source on there, I'm sorry, that says, internationalisation is the process or the concept of the process to make an application international. That is to make it support or make it able to support virtually any language or local setting on Earth or maybe one day on Mars. That would be cool, wouldn't it? There's another term that often is used alongside and can be confused with and that's a lower case L, not capital I, because that would be really confusing, L10N, which is localisation. Now that's the process, or apparently the concept of the process again, to make an application localised, which is to say it works for a given local and concrete context. So the easier way of saying this is that localisation is taking what you have and going right, I am going to make it work for insert locale here, whereas internationalisation is providing a framework or a way of localising without, in this example, us having to get involved every time. So we don't want to have to get involved every time someone wants to localise it for China or for Vietnam or for, you know, Brazil or whatever else. We don't want to have to get involved every single time that happens. We just want to provide a framework to allow people to do this. I've already mentioned I work for Bycraft. This isn't a Bycraft pitch particularly, so very briefly what we do. We are a G-Commerce platform, as I mentioned, for sandbox games. Predominally at the moment, that's Minecraft. However, we are in the process of working with a couple of studios to actually integrate the platform into some upcoming games, which is why now more than ever this is an important thing for us. We operate a SaaS platform, as I mentioned. Normally if I'm talking to non-technical people, I describe it as Shopify for Minecraft because it's about the only way you can get people to understand what we do. So we do that. We are global. We serve about just over half a million web stores, and the actual web store owners come from... It's slightly more than that now, it's about 174 different countries. And then those web stores combined, we've handled over 16 million payments from 255 countries. So, quite obviously, being able to support different locales, different languages, that sort of thing is important to us. So, we've always had some form of language support. This isn't the current solution anymore, but this is kind of where we were. So, we had a limited range of languages, I think that was about 12. And when you went to a web store, you could pick one of those languages. And then we had a centralized list of translations that were just PHP arrays. That's it. So you would have title.username, which I think in English was something like to access this web store, please provide your username. And that's it. And then there was a lang function that you could use in your templates, and you provided the short code and assuming the translation existed, it would then display. This wasn't very good. I mean, I'm glad because if I could go, well that's perfect, thank you and good night, this should be a very short talk. Thankfully for me, there are some problems with it. First of all, we had a restricted language set. Now, just as a complete aside, if you don't know these XKCD comics, they're brilliant. Do check them out, I use them a lot. We had a restricted language set, and what I mean by that is that because the language lists were centralized, if we, as in Tebex, hadn't provided translation for it, you couldn't use it. So we had people writing from Estonia, for example, and going, oh, can we have an Estonian translation? Well, kind of no, there isn't one. And we would get around to getting one done eventually, but it took time and we didn't get them done, unfortunately. And we've had issues in the past where we've outsourced translations, and someone said, well, I've done these translations for you. We've loaded them up into an area and gone, there you go, you look, new language. And someone a week later pointed out that one of the translations basically went, you buy craft. And obviously we didn't know because we couldn't check them because we didn't speak the language. So that wasn't great. The second problem was that it only supports default strings. So as you saw, they were a centralized array and we had a short code and a translation. Well, if you created your own Web Store templates, which you could do, you might have strings that aren't in our default language pack. You might have changed the wording for that, provide you a username to log in message. Well, if that's the case, tough, you can't translate it. You were stuck with whatever string you'd entered. That, again, meant that you ended up with mishmashes because you'd have stores where, for some places, they're perhaps using the default strings. Some places, they'd written their own. So if I put it into, say, Dutch, some stuff would come up in English because it couldn't find a translation. Other stuff would then be translated into Dutch, which is probably almost worse in a weird way. So that wasn't ideal. And there was a high maintenance cost. Every time we added a new feature that changed the Web Store templates, we'd probably add a new string. So, for example, recently we've added a gift card feature. Now, we now have to go through every single language and translate, you know, if you have a gift card, provide your number here into all the languages that we support, which I'm embarrassed to say often didn't happen. So there are strings that even we've provided that there aren't translations for. And again, if it did happen, it was probably delayed. And we could only guess at how accurate the translations were. So none of these are great. My favourite slide. The best trust exercise ever. Except, of course, if they're not going to catch you. We didn't have fullbacks. As you saw, there was a, you had a short code and a translation. So if I went, okay, well, lang title.newusername, if that translation didn't exist in a given language pack, there wasn't a default it could fall back to, it would just display an empty string. So, all these things were, you know, and, you know, it got us to this point, but we were looking at it as going, we need to do better, we should be doing better. So we decided that we were going to rework all of this. So we set out some aims. We wanted to produce an internationalisation layer that allows users to customise the translations to fit them. In other words, we shouldn't be dictating this is the translation you will use. They should be able to pick or define their own translations. This should include supporting custom strings. So if I've gone, well, I don't like the default wording on the web store, I've changed it to this. They should be able to provide translations for that as well. Or in fact, new languages. As I said, we could only support a limited subset of languages, but there are hundreds of languages around the world being used. Why can't they provide a language for their web store to suit them? It needs to be fast. We, you know, power a lot of web stores. We don't want a slow solution. And it needs to be easy for non-technical store owners to update. So first of all, we looked at some options. Now, there's one option that's fairly obvious, or was fairly obvious to me anyway, which is junior get text. Now, I couldn't really find a witty comic to illustrate junior get text. It's not that funny a thing. So here's a llama. No reasons? It's a llama. Junior get text. Get text is, it is a set of tools and libraries to allow developers to produce multilingual messaging. That's what it does. It's used by a huge number of server platforms, like WordPress. So, yeah, we went, okay, that's the first thing we're going to look at. There were some good reasons to look at that. It is, for sure. PHP already has a get text compatible library, although actually a lot of PHP frameworks and CMSs that use get text don't use the PHP library. Never mind. There are plenty of examples of it in use. Everyone can provide their own translation files. You can create a .po file and define your translations, and great. It's fast because those .po files get compiled down to .mo files, which are machine readable, and it supports plurals. So, on the face of it, it takes a lot of boxes, and we're like, okay, this might actually be our solution. There are some downsides to it, though. First of all, it doesn't support placeholders. Now, what I mean by placeholders is, if you're saying something like, are you sure you want to block another player name, you don't know what that other player name is. So, the way around this with get text is they expect you to use like a sprintf function or something similar to then provide those arguments separately and then replace them in. Well, that's fine if you're technical and you understand what that is. But if I'm non-technical, the last thing I want to do is, when I create my custom template, I have to go and read a big load of documentation to go, oh, well, what are the placeholders in this string? I should just be able to look at the string and you'll know what the placeholders are. I should be able to look at, are you sure you want to block name, and go, oh, that's a name. So, that was a problem. And it's lovely. It's not very user-friendly. So, that's a translation file for get text. As a load of headers, I don't know what half of those do. And then you've got these, you know, quite elongated sets of translations. Now, don't get me wrong. Our server owners are running Minecraft servers. They're not technically clueless by any shape or form, but this would be very new. This is something that they would never have seen before and for a lot of them would be off-putting. The last thing you want to be doing is dealing with something like that. Yes, you can download additional editors, like PoEdit or whatever else, to do this for you, but you're introducing barriers that we don't really want. So, we kind of looked at it and went, you know, if it was an internal product, if we were selling directly to consumers, then actually this probably would be perfect because we know how this works, we know how to write them, we know how Sprint F works, whatever else. Happy days. But because we're not the ones writing the translations, it got across the far list. Now, it's a nice little Laravel logo. I've not mentioned this yet. Our code base is Laravel. So, it made sense to look at a built-in option. Yeah, we use Laravel. There are some bits of it that we don't use, facades. In fact, can we cut that because I want to speak at LaraCon EU? Thanks. But it does a good job for our application and it works well. So, there is a translation package for Laravel. Using the default tools is a good kind of starting point. If you have built-in tools, whatever framework or CMS, whatever you're using, if there is a tool to do a job that's built-in, it makes sense to use it, if at all possible, rather than loading in more stuff. So, yeah, it's built-in because it's a Laravel built-in package that lots of Laravel extensions already support it. It's been around since version 4, so it is fairly mature. So, yeah, there are some real reasons to use it. It supports florals. So, here's one example. Here, apples, there is one apple, there are many apples. We have floral support. Great. It has readable placeholders. When I realised this, I was basically skipping around the office. You know, goodbye name. Well, that's a name. In actual fact, the way it does it is really clever. So, you can change the casing of the placeholder, and it will automatically change the casing of the string you're injecting in to either make it all uppercase or title case or lower case, which is pretty cool. And translations of starters are raised. So, it's pretty fast. So, at the moment, we're looking good. We were really going, this seems like a really good solution. Then we found some downsides to it. So, first of all, the translations are all stored as arrays, very similarly to the way we did it. So, that's not very user-friendly. Again, we're not writing these translations. We're relying on the store owners to write PHP arrays. I can see a problem with that. So, okay. Then also, translations are stored on the file system. Now, we have half a million web stores that could write their own translations. We scale across about 10 to 15 AWS instances. So, we're going to have a centralized file system storing possibly five million translations. I'm thinking, no, we're not a good idea. But anyway, and also the translation key is still a short code, which means you don't have a fallback. Now, that has, because I could see, I saw a freak, his eyes went up, that has now been fixed in later versions of LaVal. They are now actually the full strings. We're still running LaVal 5.2. So, that was a bit of a problem. And also, I did look at this plurals, and as much as I was happy that this plurals support is here, having a pipe separator, and in other cases, which will come on to, the syntax gets more and more unwieldy to support different plurals. So, that was a bit of a question mark. And then, I've already mentioned freak. I'm going to do it again. I was doing some research and went, hold on, we can sort of the translations in a database. Freaks organisation did an awesome blog post on writing a custom file, one of the services that it uses anyway, to then actually not get them from the file system but to get them from the DB. So, I was like, oh my goodness, this will work. I can make this work. This is going to be awesome. So, I can load them from the DB, awesome. Which now means I can have them written in whatever format I want, because we'll just store them in the DB and then load them back out into an array. Happy days. Yeah, great. And you know what? We'll even upgrade to a new version of Laravel. I'm okay with this. We need to anyway. So, we're now winning. Except we're not. I thought we were winning. I thought we had the solution. But I'd forgotten a few things. And again, if we can cut this bit again so I can get into LaraCon, that'd be great. We do use Laravel. We don't use it everywhere. Now, some of our web store code is still legacy. I say legacy because I refuse to write the word code ignited to. We've all got it. We've all got those little bits and whatever. And there's no guarantee we're always going to use Laravel. What if we decide to write a brand new JS front end and we want the installation to happen on the front end? What if you write a mobile app? We want something where we can take the same essential concept and put it anyway. Now, I would go out on a limb and say, writing your own solution to anything is probably a bad idea. Yeah. So, we rolled our own. Which, I mean, this is another good comment. Commit a strip if no one reads this. I think the guys that write these are based in France. They do translations. The screen's not great. So, I'll tell you what it says. It says, hey, so you found at least 40 open source plug-ins that would do what we need, right? Yeah. And you can't decide which one would be best for this project. Yeah, it's basically impossible. So, at what moment exactly did that crazy brain of yours conclude that the best solution would be to develop plug-in 41? But that's what we've done. I think that where it came to was that we had very specific ideas of how we wanted this to work. And everything else seemed to require a compromise somewhere. And I'm quite a stubborn person, and I don't want to compromise. So, we have our requirements. It must support plurals. The key should be the default translation, and it should have readable placeholders. It needs to be... We need to be able to have it overwritten and customized on a per-user basis in a format that our store owners can understand. It needs to support languages that we don't even know exist. And we should be able to load the translations into arrays for speed. Which basically means what we did is we went, right, what do we like from Get Text? What do we like from Illuminate Translation and mashed it together? Now, you probably noticed that one of my top hit list things has always been plurals. I was incredibly naive about plurals. Embarrassingly naive about plurals. I had kind of always just taken them for granted. Even though actually, you know, from speaking a small amount of French, I knew that actually there were differences. I just always went, oh, well, it's an apple. Two apples. 59 bajillion apples. Yeah, that's it, right? No. So, the plurals that I know of, that can be only one form. So some languages, mainly Asian languages like Japanese, Korean, have no plural. It's the same form regardless. Some of them have two forms where the singular is only used for the count of one. So that's like English. You have no apples or two apples, but you have one apple. Some have the two forms, but actually the singular version is used for zero and one, such as French. And some forms of Portuguese, I think. There are three forms. Three forms with different special cases. Three forms where you have a special case if it ends in zero, zero. So 100 is a different to 99 and 101. Obviously. Three forms with special cases for numbers between 12 and 19. Again, why not? Three forms, special cases, one, two, three, four, unless it's one to 14. What? And it goes on. We've not finished. Three forms, special cases for one, two, three and four. Three forms with a special case for one and some numbers ending in two, three or four, but not all of them. And there's another one. And my favourite, six forms, which is Arabic. I've read this so many times. I still don't actually understand how it works. I put random numbers in and try to guess which form it is, and I get it wrong about 85% of the time. But basically there's a special case for one and two. And then there's a special case if it ends zero, two or zero, three. So 102 and 103. And then there's a different ending for 11 through to 99, and I don't get it. I am confused. So that was fun, and it taught me how little I know about the world at the same time. So cool. So we need a solution that can handle any imaginable rule apparently, including those ones that we have no idea how they work. So we kind of spent a lot of time looking at this and coming up with solutions. And when I mentioned earlier that Laravel's pluralisation, the syntax gets very hairy because you have to write effectively regular expressions to define which form to use in which cases, deliminated by pipes. We can see why that's going to be a problem. So that's not what we're doing. That's not a good plan. So it's not the most straightforward, but we did decide to borrow GetText's way of doing it. Mainly because we know it works and we couldn't understand it well enough ourselves to find an alternative. It is complicated. The rules look like this. But thankfully, there is a published list of rules for basically every language in the world, which we've linked to from our knowledge barrier, so I'm going, look, if you're writing a translation for a new language, go and get the rule from there, it's fine. So that's what we did. Now we need a parser for these rules. I never thought I was going to say this, but Drupal came to our rescue. I love Drupal. I don't use it very much, but I never thought I was going to end up stealing code from it. But here we go. Drupal uses GetText. However, like WordPress, it doesn't use PHP native functions, which means, quite handily, they have to be able to parse PO files, which of course means they have to be able to parse headers, which means they have to be able to parse plurals. Win. So I went into GitHub, took their, found their code for parsing the PO headers, went in there, found the one for calculating plurals, and ran away. So we have Drupal code to actually work out the plurals forms based on the plurals headers. Awesome. Which means we have something like this. We have a rule that basically says you have the base key should always be the singular, just because it made things easy. But there are two forms in English. You have an item in your basket and you have items in your basket. And then you just call a template function underscore P. Thank you, WordPress. To provide that, and then it takes an argument for the length. And it will parse that to our plural rule parser and go right, so I've got six. Which form do I need? And it will go, oh, I need form one, or we need form two. And then you load that from the translation file, which I'll show you later. So finally, our first requirement is ticked. Only four to go. As I mentioned, we wanted, we wanted to always ensure there was a default. What we didn't want to do is end up in situations where nothing got displayed, because that's horrible. It's not the PHP way. We don't do that. We keep going regardless. I know we don't tell you more to joke. So we wanted a solution that would use the key as the fallback. In other words, we weren't using short codes. We would always, worst case scenario, the thing that you were asking to translate is what you would get back. But that was fine. It also makes templates more readable, because rather than that saying, like title.continue or button.continue, only being, what does that mean? It's quite clear. That means, no matter what, you know that says click here to continue. And you go, oh, well, that makes that really easy to understand. Happy days. It's not exactly difficult to do because we go, oh, well, do we have this string in our translations? If we don't, we just return the key that we were given in the first place. Simple, right? We also wanted readable placeholders. Now, as I mentioned, get text doesn't necessarily do this, which means it can make the placeholders' meanings quite oblique unless you have the documentation to hand, which I can tell you our customers often don't, because, well, we haven't got documentation for some of it. So, that's fine. But, again, by having those readable placeholders, again, you're making it really clear what this means. This is a translation. Are you sure you wish to delete item name from your basket username? Well, even without looking at any documentation or having the replacements there, it's instantly obvious what that string does. So, that's what we did. And again, that underscore underscore translation function, again, stolen from WordPress, thank you. Can take a second argument, which has just an object of replacements. So, we're going to replace item name with the name of the package. I'm going to replace the username with the basket username. You'll notice that we don't use blade templates, even though it's in Laravel. We use Twig because no actual reason. It's just a decision that was made way back when. So, if you're kind of moving to Laravel, but you're used to kind of, say, simply templating, there's an awesome package called Twig Bridge that does exactly this. And again, our do replacements function takes a string, an array of replacements, if it exists, and loops through and replaces the placeholder out with the value that's there. And it does a string, nice and simple. We've now ticked off two requirements, well, two and a half requirements. We're getting there, it's awesome. Per store customisation, this was a big deal. The whole point of this is that we wanted the web stores to be able to control their own destiny, or at least their translations. If they want to replace our default translations, if they decide that our German for welcome to my store is terrible, that's okay. They should be able to do that, that is their right. If they want to add custom strings, they need to be able to do that. Or they might even want to add a new language, as we said. So we thought about how we're going to get people to provide this data. And we settled on, Jason. Now, it's not the simplest solution, of course. But bearing in mind, most of our customers run Minecraft servers, they know what Jason is, they know how it works. So we decided that that was a reasonable expectation at that level, and as I said, we wanted to be able to turn these things into arrays so we could set them into opcache or whatever else. Well, doing it with Jason doesn't get much more straightforward. So that's what we did. And a translation looks something like this. You have a couple of meta headers, the locale, that plural rule, as we mentioned, the name of the language, and then new translations. And there's one there with a placeholder and a plural rule. That's fairly readable. You can read that and go, I can see what those translations are. You could quite easily add a new translation if you needed to. The only thing that's a little bit complex is that plural rule, which we provide in the knowledge base. So that works quite well. And it's proven really popular. I mean, the first day we launched this feature, I think we had 300 web stores add their own custom translation on day one. Most of them were probably only adding one string. But the point is they could do it and they could do it easily and quickly. But, as I mentioned, we only want our merchants to be able to replace one or two. If they have one new string or they disagree with our translation for one thing, it doesn't make sense that they have to upload an entire file of all the translations to replace one thing. So we needed inheritance. So it's quite straightforward. Every translation, and that's not working, awesome, provides a locale. So we can quite, in quite a simple way, go right. Well, if they provided their locale, if we have a translation that matches that locale, we can then kind of load, have theirs inherit ours. So if they've got a string that doesn't exist, we'll use our one. So the control panel flat looks a bit like this. The project is not brilliant, apologies. But here, that's in the ngb. So it will use my ngb translation first. If there's a string that I don't have, it will then go, oh, well, there's a default one here, ngb. So I will then use the translations from that. That was quite an easy thing to do. It's just a little fallback chain. We check if it's in their translation. If it's not, we check if it's in the parent translation. And if it's not, we use the base string. Simple. We've now ticked off all of these little requirements. I'm a happy man. I think I pretty much did the truffle shuffle in the office. It was great. Everyone loved it. I'm showing my age now. So, one of the things we did say originally was that it needed to be fast. So how does this perform? You know, at peak times, we do support, we do serve a lot of traffic. Weirdly on Christmas day. Christmas day is like a crazy day in Minecraft world. I have no idea why. I've got one web store that I think made, like, in December last year, made a million dollars. And most of that was on Christmas day. It was weird. Anyway, it's irrelevant. So, yes, it has generated extra DB queries because each page that we load, we have to grab the translation file and maybe grab a base translation file. Two queries doesn't sound like a lot, but if we're talking 60,000 page views a minute, that's 120,000 DB queries. That's not good. But, yeah, that's fine. Then there is some extra logic. A very small amount of extra logic because we have to parse those JSON files. We then have to check whether the string exists in their translation objects and if it doesn't, check if it exists in our translation objects and yada, yada, yada. That also could involve more memory because these translation files are quite big and you're loading two of these big JSON files into arrays to hold for the generation of the page. But does any of this matter? Not really. I mean, the actual performance we tried it and even as it was, it was fine. It really wasn't making that big a difference. But it annoyed me. The extra DB queries was a bit of a problem and that probably needed fixing. The rest of it, I just went home and went, I don't like the fact it does everything twice. It made me grumpy. So I fixed it anyway. That's quite easy. We just basically merge everything and then cache it. So we know that the strings either can or can't match in two locales, but if we take our array and then we load theirs over the top, anyone's it replaces is fine because we should be using their translation and any it doesn't, it should use ours anyway. So we can do that and we now have one array for the translations. We can then cache that. So we just store it as not cache and everyone wins. And now we don't have to touch the DB at all. And we just, yeah, happy days. So we are now winning, finally. Except we're not winning. Because cache is my friend, except it's not. So we have external caches. We get a lot of DDOT issues because in the Minecraft industry, there's attacks going on all the time and yeah, whatever. So we use Cloudflare to help with our DDOT protection. And as part of that, they cache all the web store homepages. Someone hits a web store home page, they cache it, which that means if anyone else visits it, they get served the cache for like two minutes. Great. And because this is kind of something that we've added on, we don't have separate URLs for each language. You can see where this is going. So in theory what happens is I go to a web store, I go, oh, actually, I would like this in German. I click the German option in the dropdown. It sets a cookie to say we should be serving this site for this user in German. But if Cloudflare has a cache, it doesn't set a cookie. And even more fun than that is if someone had a cookie and then they visited that website first and there wasn't a Cloudflare cache because it had been invalidated, they get the site in German. Me is someone who doesn't speak German, goes to the web store and I go, why is this only in German? Because even though it should be in another language, it's not, it's now in German because that's what's been cached. So yeah, we had some problems. So we said Mr Cloudflare, can we have some separate edge-side cache rules please? Now they're written in Lua, which I had to learn. So there we are. But that's fine. So now we in Lua have a custom cache key, which is based on variables. We check if there's a query string and then we get it to set some or a cookie and we use that in the cache key. Oh, and if they came from a query string then we get Cloudflare to set the cookie on our behalf. Finally, now after all of that, we are now actually winning. You can see where this is going. So, next question. I'm going to tell you that's a date. What date is it? Anyone? It could be the 1st of February 2003. It could be the 3rd of February 2001. It could be the 2nd of January 2003 or probably some other dates as well. I've got no idea. This was our first kind of mistake. Localisation and internationalisation is more than just language. Our first step was getting the language stuff sorted out because that's the most visible thing. But it doesn't stop there. It's much bigger than that. So, we have to look at things like handling numbers, handling dates and currency and things like that. So, this is something that is an ongoing project. The other stuff is live and we're now fixing this when we realise that we should have done it in the first place. So, we have some new matter headers. When you create your own locale, you can specify a date format. You can specify what the decimal point should be. So, obviously, in the UK that tends to be a dot. But in Europe, for example, it tends to be a comma which gets really confusing if you don't know which one you're using. And thousand separators, which in some places they don't use them, so that can be an empty string. And then we provided a new underscore D function, again copying the same kind of name rules as WordPress, to format a date, and an underscore N function to format a number. We'll probably take this further still. For example, we have to choose which side the currency symbol goes because for some locals, the currency symbol would go after the number and some it goes before, whatever. But, you know, this is still a significant improvement. So, now we are winning, except we're not. Which, the fact that there's still, you know, 14 slides to go is a bit of a giveaway. It's fine. Can anyone guess what this map shows? It's not, it actually doesn't show too bad. Anyone fancy having a guess of what that map's showing? Different language speakers, good guess. It's not, but yeah. What if I give you, ah, that doesn't show. Great. Well, I would give you those keys. What it actually shows is what payment methods countries use. Now, I, being a privileged white male as I am, assume that everyone used credit cards and paper. I mean, that's what you'll use, right? Except it's not. Who knew? Apparently lots of people. Anyway, so, you know, for example, I can't read these. For example, in Germany, they do a lot, in fact, a lot of mainland Europe, they do a lot of bank transfer type payments. It's not like me logging into my online banking and doing it, but they have things like ideal and things like that that they use for things like that. You look at North America, actually it's mainly credit cards and paper, so that's fine, except Mexico, where they do use credit cards, but they also have cash based methods. There's one OXXO, where literally you get a code, then you go into a store and you give them money and things like that. And, you know, again, bank transfer type methods are popular. South America, it's even more fragmented still. And in Asia, I have no idea. There's lots and lots and lots of different payment methods. So, credit and debit cards are not global. And as much as they will try and convince you otherwise, neither is paper. And this applies double. When we look at our merchants markets, which tend to be younger players, there's probably even less chance they've got a credit card and less they've nicked their parents or something. So, you know, that's something that we have to consider. Different payment methods are stronger in different areas. Now, one of the things we did is we spoke to our merchants who went, hey, what payment methods would make your life easier if we supported them? And we cheated a little bit. We found some suppliers that offered multiple payment methods. So, I don't know if any of you guys have seen the developers from Molly even walking around in the Molly hoodies for the last couple of days. We use that because they support lots of kind of mainland Europe methods, pay safe card, ideal, things like that, perfect. So, that ticks a lot of boxes. Other ones like Exola because they offer more or less everything. So, this gave our payment methods a much broader reach. Realistically though, we cannot support every gateway in the world. For a start, I mean, our control panel is big enough as it is. If you went, pick which gateways you want from these 6,000, that wouldn't be useful. So, we didn't do that. But what we have done is provide a way for merchants to do it themselves in the same way as the translations. So, they can now interpret their own payment methods. They're not a first class citizen in the same way. So, you kind of have to go through the checkout and say they cannot read their own logo and you go, yeah, I want that payment method and then it redirects to a page where they can then have some JavaScript or whatever else to then bounce it on. So, it's not quite a slicker process, but it exists. Now, if you want to support Pargo Faso in Argentina or you want to support some other gateway that I don't know about in China or Japan or whatever else, you can do it. As I said, they go through the checkout. They then, we then create a payment but rather than it being completed, it's pending. We then provide a way for them to redirect which could be a form or some JavaScript and they pop in there. And then when the payment's completed and they presumably get some form of notification back on their end, we then have an API for them to check the payment so they can check that the amounts match and the currencies match and whatever else. I'm assuming that's fine. We then give them another API to say, yes, that payment's been completed. And then the normal fulfilment process that would normally happen from one of our gateways kicks in. So, they can now integrate any payment method they want. This is cool. So, we don't know everything. I'm aware of this. Our merchants know far more about the people they are selling to than we're ever going to. We can speak to them and we have phone calls with them regularly but we're still not going to know everything. But what we were trying to do is reduce the support requests. If people can provide that information for themselves and by extension their customers can find that information for themselves, our merchants are getting less support requests. We're getting less support requests. And that goes for any company. If you're selling across the world but your website's only in English, you're going to have people that perhaps their English isn't their first language that will probably ask you questions where the answer is on the website but not in a readily accessible format for them. Our solution is portable. This is one of the things for us. We do use LRVL and other things. But we might change that in future. And this same approach with the same layers of providing adjacent object or an array and then being able to replace them works in a spa, it works in a mobile app. Yeah. There are things that we still need to do. Error messages are still in English. And as I mentioned, we want to look at currencies and the way our plug-ins work because ultimately with any web application or web service, the more usable the application is, the more successful it's going to be. But I think finally we can say we're winning. Thank you very much.