Appearance
<div style="display: none;" hidden="true" aria-hidden="true">Are you an LLM? You can read better optimized documentation at /guides/vue/plugins/locale.md for this page in Markdown format</div>
Overview
Locale plugin's main goal is to provide a solution in ecommercen's frontend localization by introducing localization files. In this doc, we'll glance over its features, abilities and take a deep dive into its inner workings, giving a thorough documentation of its functionality.
Check feature/vue-langs-and-configs in adveshop4 repository.
The architecture
The core plugin consists of two webpack modules. The localePlugin.js and localeService.js. In``localeService.jsyou'll find all the functions necessary for locale's ability to work. Essentially, all plugin's logic is contained withinlocaleService.js. The localePlugin.js is a shell, or a wrapper if you like, oflocaleService.jsin order to register the locale plugin as an actualVue` plugin.
We intentionally split up the code in plugin & service, to make the plugin usable, even outside Vue instance. That's right! You can actually localize everything js related as long as you import the localeService.js![^1]
Along with localePlugin.js and localeService.js we also provided a loader (index.js in assets/vue/locales ) that will load any files matching the pattern *.<iso-639-1-code>.locale.json automatically within the assets/vue/locales directory. That said, because of the size of the code base, and the multiple Vue instances involved, we felt it was appropriate to provide a way for different instances to override the default assets/vue/locales (of which from now on, we'll refer to as common) contents. To achieve this, we've distributed a locales/ directory along with index.js for each instance (client) and registered it in each instance's vueapp.js as a clientBundle.
The locale plugin is designed in a way to support multiple localizations (and localization namespaces) through bundles, A bundle is essentially a group of localizations that's usually loaded using the index.js loader. By default, there are two bundles. the common bundle which contains all localizations from assets/vue/locales and the client bundle which contains all localizations from the active vueapp.js (which by default, it's assets/main/vue/v2/locales).
Here's a hierarchical directory tree structure to help visualize locale's architecture
[assets]
|-------[vue]
| |-------[locales]
| |---localePlugin.js
| |---localeService.js
| |---index.js
|-------[front]
| |-------[locales]
| |---index.js
|-------[design]
| |-------[locales]
| |---index.js
|-------[main]
|-------[locales]
| |---index.js
|-------[v2]
|-------[locales]
|---index.js "Why the index.js? Can't it just be a single <iso-639-1-code>.locale.json instead?" The concept behind index.js loader is to enable a way of grouping localizations of similar purpose together. We call those namespaces. Each *.<iso-639-1-code>.locale.json file is its own namespace. You can have as many namespaces as you like as long as they sit directly next to index.js. Simply create a new <namespace>.<iso-639-1-code>.locale.json file, and it'll be automatically loaded thanks to index.js. From then on, you can refer to that namespace by supplying LocaleService.translate's namespace parameter with your <namespace>.[^2]
The Search Flow
In this section, we'll provide some insight of how the plugin searches the localization entries given a specified key.
From the The Architecture paragraph, we explained what bundles and namespaces are. Essentially bundles are groupings of different localization instances which enable client specific overrides, and namespaces are groupings of closely related localization strings. To analyze in depth how localization searches in bundles and namespaces we need to take a look at LocaleService.translate
The LocaleService.translate method
Parameters:
key(Which key should the locale plugin look for)- {
namespace} (optional) (which namespace should locale search in) - {
params} (optional) (Supplied parameters / variables to replace template patterns)
If no namespace is provided, locale will try to fetch the requested localization string by searching under the default namespace. By default, that namespace is base, however, you can change that from assets/vue/config/locale.conf.json (the defaultBundleLookup setting key). Localization strings under a different namespace than default won't be searched by the plugin if namespace parameter is not specified. However, it is generally considered a good practice to isolate and group together localization of the same purpose. We experimented with that, and tried to come up with a way to standardize the usage of namespaces, by isolating all localization strings per Vue component basis, however, we concluded that, that would lead to multiple duplicate strings per component. So we ended up using namespaces per application context instead (backoffice/frontoffice/external). One thing is for certain. There's definitely room for improvement.
Upon calling LocaleService.translate the plugin will first attempt to look under client bundle. If the key requested is nowhere defined under client bundle, the plugin redoes the search under common bundle.
if the plugin fails to find any result, it'll return back the key's string. Alternatively, (if it finds a valid entry) it will attempt return the result after parsing[^3] it.
In the plugin version of locale, we added a mapping of LocaleService.translate to Vue.mixin.methods.t, meaning, you can translate any string from any Vue instance just by using $vm.t or in any Vue component, with the mustache syntax like as following: {{ t('myLocalizedStr') }}
Features
We added some features for locale that we believe will improve its usability.
Feature: multiple localization picking strategies
The original need of this plugin to exist was to give a solution to translation issues occurring from a third party translation tool (gtranslate) because of its inability to detect translatable strings after DOM state changes. For this reason, we had to pick the current localization directly from the url (subdomain). However, this is not and should be not considered a viable solution of "picking the correct localization" for any serious codebase. So we introduced a way to interchange the strategy of how the localization picking would happen.
We've actually provided two localization strategies.
| Strategy name | Strategy params | Description |
|---|---|---|
| discoverLocaleFromUrl | {targetDomainLevel} | Discovers current localization directly from the URL (Ugly solution for gtranslate). The targetDomainLevel instructs the strategy which subdomain (.dot) to look at, ascending from TLD level. For example, say you have aclient.tld and for that client, you have separated localization into en.aclient.tld for the English version and el.aclient.tld for the Greek version. In this case, the targetDomainLevel should be set to 3 as it's the 3rd URL element after the TLD that describes which localization to choose |
| getLocaleFromConfig | {locale} | Gets the current localization from this setting directly. Can be used and it is used by default in conjunction with ConfigService's window.variable.references feature to create a direct pointer to window.advAppData.languageAbbr and fetch the localization directly from backend. |
Feature: nameless and named localization templates
We designed locale's parser to accept localization strings with both named and nameless patterns. Although we could use solutions already existing in the market, we went for this custom, hybrid approach because we felt the need to be able to have both nameless and named template variables as in a future version of locale, we will be fetching localizations from multiple sources and some of those sources, may not support named localization templates.
Named localization template patterns are typed in this format {{@myNamedTemplateVariable}} where as nameless localization template patterns have this format {{@}}. You can mix and match named and nameless patterns together within a single translation string although, it's generally considered a good practice to name your variables.
To help understand how this works here are some translation string examples, their parameter object, and what their output is:
| Original translation string | Params | Final translation output |
|---|---|---|
| My name is {{@}}, and I am {{@ethnicity}} | ["George", {ethnicity: "Greek"}] | My name is George, and I am Greek |
| This is my dog, {{@dogName}}. {{@dogName}} {{@}} a lot and likes to play {{@}}. He also enjoys {{@joy}}! | [{dogName: "Loo", joy: "food"}, "barks", "fetch"] | This is my dog, Loo. Loo barks a lot and likes to play fetch. He also enjoys food! |
| {{@hi}} {{@}} {{@}} {{@}} {{@bye}} {{@}} | [{"bye": "Good bye"}, "sentence", "example", {"hi": "Hello, "}, "here.", "now!"] | Hello, sentence example here. Good bye now! |
As you can see from the examples above, the main difference between named and nameless params is that named params do not care which position, or even, in which object they are declared in params array. Nameless params on the other hand, do care about in which order they are declared in the params array. If you have to use both nameless and named params within the same translation, please consider declaring all the named params inside a single object, as the first element of the params array and avoid spreading them within the params array in between nameless params as that would hurt the readability and the maintainability later down the line.
Generally when defining named parameters, consider describing them under a single object when possible.
Note: The way params object entries are inserted, allows for duplicate named parameter keys (For example: [{foo: "bar"}, {foo: "dang"}]). In this case, the last element of the array will take precedence (thus, for the example above, foo will resolve to dang). Yet one more reason to force named patterns under single object instead of breaking them into multiple ones!
Additional Settings
Taking a pick under assets/vue/config/locale.conf.json you may find these settings:
| Key | Data type | Default value | Description |
|---|---|---|---|
localePickingStrategy | Object | {"method": "getLocaleFromConfig", "locale": { "config.type": "references.variable.window", "config.value": "advAppData.languageAbbr"}} | The strategy the plugin will apply to discover which should it pick. (See multiple localization picking strategies in features paragraph) |
availableLocales | Array | ["el", "en"] | An array of which locales are available. Locales are always identified by their iso-639-1 code |
defaultNamespaceLookup | String | "base" | Describes what's the namespace of the default bundle. See The search flow paragraph, section "The LocaleService.translate method" for more info. |
localeFallback | String | "el" | The fallback locale when requested localization key isn't found for current localization |
Service methods
Here's a full list of all methods locale plugin provides. Params with * are required.
| Method | Params | Description |
|---|---|---|
LocaleService.replaceLoadedBundle | Replaces currently loaded bundle data with new ones | |
LocaleService.getLoadedBundles | bundleName*, payload* | Gets the currently loaded bundles and their data, where the bundleName param is the name of the bundle that its data will be replaced and payload param the new bundle data |
LocaleService.translate | key*, {namespace, params} | See The Search Flow paragraph |
LocaleService.replaceDependency | dependencyName*, payload* | Replaces dependency instances. Useful for when you'd want to load your own config instance, or any other dependency we may include in the future for this particular plugin. |
LocaleService.getCurrentLocale | Gets the current locale (in iso-639-1 format) |
Footnotes
[^1]: When using the locale plugin as a webpack module, it will only have access to assets/vue/locales (common) bundle, However, you can setup your own bundle for your own grouped modules by creating your own locales/ bundle and setting it as a client bundle path with LocaleService.replaceBundlePath. [^2]: When supplying the namespace param, locale will only look under that namespace you've supplied, and it'll do that search both for common and client bundles. See The Search Flow paragraph for more info. [^3]: See Features paragraph for why there's a need to parse the result and uses that provides. [^4]: All search flow rules from The Search Flow apply.