Drawbacks of using singleton with models - javascript

So let's say I'm making a React Redux app for handling a library. I want to create an API for my backend where each model (book, author, etc) is displayed in the UI.
Each model does not provide a public constructor, but a from static function which ensures that only one instance per id exists:
static from (id: string) {
if (Books.books[id]) {
return Books.books[id];
}
return Book.books[id] = new Book(id);
}
Each model provides an async fetch function which will fetch its props using the backend. The advantage is that there is no thousands instances, also I don't have to fetch twice (if two parts of my app needs the same model, fetch will actually be called only once). But I fail to find any drawbacks, except that there might be a discrepancy between a code that fetches its models and one that assumes they are still not fetched, but I still don't see when it would really be an issue

But I fail to find any drawbacks
I see at least two :
The singleton pattern is an anti pattern.
Static factory methods don't provide explicit dependencies.
Mocking the method in unit tests or switch to another implementation will be harder.
You don't have cache size limitation.
For short lists, it is OK.
But if you may cache many objects, you should keep only last recently used instances.

I can think of two problems:
Are your models mutable? If you change a property of the instance, it would reflect everywhere that instance is used. That might either be desirable, or not at all. And with that from method, you cannot do anything about it.
If your models are immutable, sharing instances is in fact a common practice, also known as hash consing.
Your implementation leaks memory like hell. The instances will stay referenced from that books array/object even if they are no longer needed.

Related

Can a nuxtjs application store be something dynamic?

When we work with an API, logically, we should split our "store" folder into several files, cutting our logic across our different entities.
For example, an API that deals with users as well as books.
Store/
books.js
users.js
It is VERY likely that at some point in our application, we will have to search the data in bulk. (Either all users or all books). Or more specifically (only 1 book, only 1 user; identified by an ID).
My question is the following:
Can I afford to make fewer files in the store, if the methods I use to retrieve the books are also used to retrieve the users?
For example, I could have a generic method, which would fetch "entity" passed as a parameter.
async genericFetch({commit}, entity) {
const req = await this.$axios.get(`/${entity}`)
commit('setData', {entity, values: req.data})
}
I will use this method when mounting a component, e.g.
We can imagine the same thing with getters or mutations (we can imagine that in my example, the commit setData will precisely modify the data according to the entity passed as a parameter as well).
I'd be happy to know how you organize your store.
According to some projects, it's not uncommon to have to repeat ourselves through the different files in the store, only because we're not going to work with the same data, although we're doing pretty much the same thing.
Nevertheless, the question I'm asking suggests a real problem of clarity in the code (and therefore, I imagine it would be more complicated to maintain the code).
Thank you for your feedback!

Accessing private variables defined with WeakMap inside a derived class

I'm using the common WeakMaps pattern to emulate private variables inside es6 classes, but I cannot find a way to have "protected" variables, meaning variables that are private and that can be accessed through derived classes, eg:
var Window = (function() {
const _private = new WeakMap();
const internal = (key) => {
// Initialize if not created
if (!_private.has(key)) {
_private.set(key, {});
}
// Return private properties object
return _private.get(key);
};
class Window {
constructor() {
// creates a private property
internal(this).someProperty = "value";
}
}
return Window;
})();
If I create a subclass using the same pattern, how can I access someProperty in the subclass without having to define a getter method in the base class (thus completely defeating the whole purpose of having weakmaps for private properties) ?
If there's no elegant solution by using this pattern, what would be the best course of action to take?
I'm building a webapp which can have various "layered windows" displaying various products, loaded from a different script that makes few requests to .php endpoints to gather this information.
The library itself is not intended to be a public library for everyone to get access to, at most other team-mates might have to edit parts of it but they would still respect the defined patterns/conventions
from a security standpoint most requests to other APIs would be done from a separate script handling validation of the payload so what I'm really trying to accomplish is to make reusable Window classes that can use some sort of "protected" variables across derived classes since it would definitely help me in the process of building this particular type of GUI
The library itself is not intended to be a public library for everyone to get access to, at most other team-mates might have to edit parts of it but they would still respect the defined patterns/conventions
From the description of what you're really trying to do that you added to your question, it sounds like this isn't a "security" issue per se, but rather you're looking for the best programming implementation/convention for your local team that will be using this interface so that it will be clear to other developers which state is "protected" and for use only inside the implementation and not from the outside consumers of the objects.
If that's the case, I'd just go with the underscore convention where a property name on the object that starts with an underscore as in this._someProperty is meant only for internal use in the methods of the object itself (analogous to "protected" members in C++) and not for external use by consumers or users of the object.
Then communicate that in the doc for the implementation and verbally with the team you work with to make sure everyone not only understands that convention in the code you write, but so that they can also consistently use the same convention in their code.
Since it doesn't appear you have an actual security need here, the reasons to go with this type of leading underscore "convention" instead of more involved solutions that provide some real protection of the data from other developers (like what you were attempting to do):
Implementation is simpler
There is no performance degradation
Does not interfere with modularity and putting derived classes in separate files
Infinitely extensible to as many properties, as many classes
Easier to educate the team you're working with on how to do it
A saying once shared with me by a senior developer was that "my code should be as simple as it can be to meet the objectives (correctness, stability, testability, maintainability, extensibility and reuse)". This helped me to strive for simplicity in implementation and avoid over designing beyond what is actually needed.

Is it good practice to encapsulate mutators in a class inside a JS Module?

We're being as functional as possible with our new product using JavaScript. I have an Authentication module that has a tokenPromise which is updated whenever the user logs in or the token is refreshed. Seems we have to allow mutation.
Instead of putting tokenPromise at the module level, I've created a class that only contains high-level functions that limit how the state can be mutated. Other helper functions which are pure (or at least don't need to mutate state) are outside the class. This seems to help a lot in reasoning about when the member might change - it is colocated with all operations that might change it.
I haven't found other examples of such a pattern - is this considered good practice, or is there another way we should be looking at? Here's the class which contains the mutable data, which is exported from Authentication.ts.
export default class Authentication {
public static async getAuthToken(): Promise<string> {
if (!this.tokenPromise || await hasExpired(this.tokenPromise)) {
// Either we've never fetched, or memory was cleared, or expired
this.tokenPromise = getUpdatedTokenPromise();
}
return (await this.tokenPromise).idToken;
}
public static async logOut(): Promise<void> {
this.tokenPromise = null;
await LocalStorage.clearAuthCredentials();
// Just restart to log out for now
RNRestart.Restart();
}
private static tokenPromise: Promise<IAuthToken> | null;
}
// After, at the module level, we define all helper functions that don't need to mutate this module's state - getUpdatedAuthToken(), etc.
A possible principle seems to be: keep objects with mutable state as compact as possible, exposing only high-level compact methods to mutate state (e.g. logOut and refreshAuthToken, not get/set authToken).
I've created a class that only contains high-level functions that limit how the state can be mutated. This seems to help a lot in reasoning about when the member might change - it is colocated with all operations that might change it.
Yes, this is a standard best practice in OOP - the separation of concerns by encapsulation of state changes into the object. No other code outside the object (the class) may mutate it.
Other helper functions which are pure (or at least don't need to mutate state) are outside the class.
I wouldn't go that far. You should put helper functions (methods?) that belong to instances on the class as well, or at least in its direct vicinity - putting them in the same module might be good enough though. Especially when they access "private" parts of the objects. To just distinguish pure from impure functions, you might also use conventions such as get prefixes for pure methods.
An alternative to that is providing a separate immutable interface for your class that contains only the pure methods. You could have this as a second class declaration, and use one method to convert between the representations.
exposing only high-level compact methods to mutate state
I think that's not completely true. You are also implicitly exposing some way to access the state (which the pure helper functions would then use), right? You might as well make those explicit.
When dealing with mutable state, the order of writes and reads does not only matter for the internal view (which parts change when) but also on the external (when does the whole object state change). Some convention, like "properties (and getters) are pure, methods might be impure" will help a lot.

Marionette Controller Function Declarations - Best Practices

I'm writing a large scale marionette application, which is ran initially from a router / controller.
Here's my question -- is it good practice to include other functions in your controller that aren't meant for routes?
So say I have the following method:
index : function() {
alert('test!');
}
is it consistent with best practices to be able to declare other functions in the controller not called when routes are initialized? Like so:
noRouteAssociated: function() {
alert('test!');
}
index: function() {
this.noRouteAssociated();
}
Obviously this is a simplified example, but I am trying to avoid putting large amounts of information or function declarations inside of methods only because they're associated with routers.
The roles and responsibilities of controllers are best illustrated by #davidsulc in this SO post and better yet his new book.
Generally speaking, it's okay to include methods that aren't meant for routes, if they're controlling the workflow of your app. Event triggering is a good example, but if you want to change the appearance of something or retrieve data from a database, you should move these methods to a view or model, respectively. The block quote below is taken directly from Marionette's controller documentation.
The name Controller is bound to cause a bit of confusion, which is rather unfortunate. There was some discussion and debate about what to call this object, the idea that people would confuse this with an MVC style controller came up a number of times. In the end, we decided to call this a controller anyways, as the typical use case is to control the workflow and process of an application and / or module.
But the truth is, this is a very generic, multi-purpose object that can serve many different roles in many different scenarios. We are always open to suggestions, with good reason and discussion, on renaming objects to be more descriptive, less confusing, etc. If you would like to suggest a different name, please do so in either the mailing list or the github issues list.

Durandal (knockout) app with multilanguage support

I am building multilingual support for the app I'm working on. After doing some research and reading SO (internationalization best practice) I am trying to integrate that in a 'framework-friendly' way.
What I have done at the moment is following:
Created .resource modules formatted like so:
resources.en-US.js
define(function () {
return {
helloWorlLabelText: "Hello world!"
}
});
On the app.start I get the resource module with requirejs and assign all data to app.resources. Inside of each module specific resource is assigned to observables and bonded with text binding to labels and other text related things. Like so:
define(function (require) {
var app = require('durandal/app'),
router = require('durandal/plugins/router')
};
return{
helloWorldLabelText: ko.observable(app.resources.helloWorldLabelText),
canDeactivate: function () {
}
}
});
On the view:
<label for="hello-world" data-bind="text: helloWorldLabelText"></label>
The resources are swapped just by assigning new module to app.resources.
Now the problem is when the language is changed and some of the views have been already rendered, the values of previous language are still there. So I ended up reassigning observables inside of activate method. Also tried wrapping app.resources into observable, but that didn't work either.
I don't think I ended up with the most clean way and maybe anybody else had some other way that could share. Thanks.
For those who are still confused about best practices, those who feel that something is lacking, or those who are simply curious about how to implement things in a better way with regard to Durandal, Knockout, RequireJS, and client-side web applications in general, here is an attempt at a more useful overview of what's possible.
This is certainly not complete, but hopefully this can expand some minds a little bit.
First, Nov 2014 update
I see this answer keeps being upvoted regularly even a year later. I hesitated to update it multiple times as I further developed our particular solution (integrating i18next to Durandal/AMD/Knockout). However, we eventually dropped the dependent project because of internal difficulties and "concerns" regarding the future of Durandal and other parts of our stack. Hence, this little integration work was canceled as well.
That being said, I hopefully distinguished generally applicable remarks from specific remarks below well enough, so I think they keep offering useful (perhaps even well needed) perspectives on the matters.
If you're still looking to play with Durandal, Knockout, AMD and an arbitrary localization library (there are some new players to evaluate, by the way), I've added a couple of notes from my later experiences at the end.
On the singleton pattern
One problem with the singleton pattern here is that it's hard to configure per-view; indeed there are other parameters to the translations than their locale (counts for plural forms, context, variables, gender) and these may themselves be specific to certain contexts (e.g. views/view models).
By the way it's important that you don't do this yourself and instead rely on a localization library/framework (it can get really complex). There are many questions on SO regarding these projects.
You can still use a singleton, but either way you're only halfway there.
On knockout binding handlers
One solution, explored by zewa666 in another answer, is to create a KO binding handler. One could imagine this handler taking these parameters from the view, then using any localization library as backend. More often than not, you need to change these parameters programmatically in the viewmodel (or elsewhere), which means you still need to expose a JS API.
If you're exposing such an API anyway, then you may use it to populate your view model and skip the binding handlers altogether. However, they're still a nice shortcut for those strings that can be configured from the view directly. Providing both methods is a good thing, but you probably can't do without the JS API.
Current Javascript APIs, document reloading
Most localization libraries and frameworks are pretty old-school, and many of them expect you to reload the entire page whenever the user changes the locale, sometimes even when translation parameters change, for various reasons. Don't do it, it goes against everything a client-side web application stands for. (SPA seems to be the cool term for it these days.)
The main reason is that otherwise you would need to track each DOM element that you need to retranslate every time the locale changes, and which elements to retranslate every time any of their parameters change. This is very tedious to do manually.
Fortunately, that's exactly what data binders like knockout make very easy to do. Indeed, the problem I just stated should remind you of what KO computed observables and KO data-bind attributes attempt to solve.
On the RequireJS i18n plugin
The plugin both uses the singleton pattern and expects you to reload the document. No-go for use with Durandal.
You can, but it's not efficient, and you may or may not uselessly run into problems depending on how complex your application state is.
Integration of knockout in localization libraries
Ideally, localization libraries would support knockout observables so that whenever you pass them an observable string to translate with observable parameters, the library gives you an observable translation back. Intuitively, every time the locale, the string, or the parameters change, the library modifies the observable translation, and should they be bound to a view (or anything else), the view (or whatever else) is dynamically updated without requiring you to do anything explicitly.
If your localization library is extensible enough, you may write a plugin for it, or ask the developers to implement this feature, or wait for more modern libraries to appear.
I don't know of any right now, but my knowledge of the JS ecosystem is pretty limited. Please do contribute to this answer if you can.
Real world solutions for today's software
Most current APIs are pretty straightforward; take i18next for example. Its t (translate) method takes a key for the string and an object containing the parameters. With a tiny bit of cleverness, you can get away with it without extending it, using only glue code.
translate module
define(function (require) {
var ko = require('knockout');
var i18next = require('i18next');
var locale = require('locale');
return function (key, opts) {
return ko.computed(function () {
locale();
var unwrapped = {};
if (opts) {
for (var optName in opts) {
if (opts.hasOwnProperty(optName)) {
var opt = opts[optName];
unwrapped[optName] = ko.isObservable(opt) ? opt() : opt;
}
}
}
return i18next.t(key, unwrapped);
});
}
});
locale module
define(function (require) { return require('knockout').observable('en'); });
The translate module is a translation function that supports observable arguments and returns an observable (as per our requirements), and essentially wraps the i18next.t call.
The locale module is an observable object containing the current locale used globally throughout the application. We define the default value (English) here, you may of course retrieve it from the browser API, local storage, cookies, the URI, or any other mechanism.
i18next-specific note: AFAIK, the i18next.t API doesn't have the ability to take a specific locale per translation: it always uses the globally configured locale. Because of this, we must change this global setting by other means (see below) and place a dummy read to the locale observable in order to force knockout to add it as a dependency to the computed observable. Without it, the strings wouldn't be retranslated if we change the locale observable.
It would be better to be able to explicitly define dependencies for knockout computed observables by other means, but I don't know that knockout currently provides such an API either; see the relevant documentation. I also tried using an explicit subscription mechanism, but that wasn't satisfactory since I don't think it's currently possible to trigger a computed to re-run explicitly without changing one of its dependencies. If you drop the computed and use only manual subscription, you end up rewriting knockout itself (try it!), so I prefer to compromise with a computed observable and a dummy read. However bizarre that looks, it might just be the most elegant solution here. Don't forget to warn about the dragons in a comment.
The function is somewhat basic in that it only scans the first-level properties of the options object to determine if they are observable and if so unwraps them (no support for nested objects or arrays). Depending on the localization library you're using, it will make sense to unwrap certain options and not others. Hence, doing it properly would require you to mimic the underlying API in your wrapper.
I'm including this as a side note only because I haven't tested it, but you may want to use the knockout mapping plugin and its toJS method to unwrap your object, which looks like it might be a one-liner.
Here is how you can initialize i18next (most other libraries have a similar setup procedure), for example from your RequireJS data-main script (usually main.js) or your shell view model if you have one:
var ko = require('knockout');
var i18next = require('i18next');
var locale = require('locale');
i18next.init({
lng: locale(),
getAsync: false,
resGetPath: 'app/locale/__ns__-__lng__.json',
});
locale.subscribe(function (value) {
i18next.setLng(value, function () {});
});
This is where we change the global locale setting of the library when our locale observable changes. Usually, you'll bind the observable to a language selector; see the relevant documentation.
i18next-specific note: If you want to load the resources asynchronously, you will run in a little bit of trouble due to the asynchronous aspect of Durandal applications; indeed I don't see an obvious way to wrap the rest of the view models setup code in a callback to init, as it's outside of our control. Hence, translations will be called before initialization is finished. You can fix this by manually tracking whether the library is initialized, for example by setting a variable in the init callback (argument omitted here). I tested this and it works fine. For simplicity here though, resources are loaded synchronously.
i18next-specific note: The empty callback to setLng is an artifact from its old-school nature; the library expects you to always start retranslating strings after changing the language (most likely by scanning the DOM with jQuery) and hence the argument is required. In our case, everything is updated automatically, we don't have to do anything.
Finally, here's an example of how to use the translate function:
var _ = require('translate');
var n_foo = ko.observable(42);
var greeting = _('greeting');
var foo = _('foo', { count: n_foo });
You can expose these variables in your view models, they are simple knockout computed observables. Now, every time you change the locale or the parameters of a translation, the string will be retranslated. Since it's observable, all observers (e.g. your views) will be notified and updated.
var locale = require('locale');
locale('en_US');
n_foo(1);
...
No document reload necessary. No need to explicitly call the translate function anywhere. It just works.
Integration of localization libraries in knockout
You may attempt to make knockout plugins and extenders to add support for localization libraries (besides custom binding handlers), however I haven't explored the idea, so the value of this design is unknown to me. Again, feel free to contribute to this answer.
On Ecmascript 5 accessors
Since these accessors are carried with the objects properties everywhere they go, I suspect something like the knockout-es5 plugin or the Durandal observable plugin may be used to transparently pass observables to APIs that don't support knockout. However, you'd still need to wrap the call in a computed observable, so I'm not sure how much farther that gets us.
Yet again, this is not something I looked at a lot, contributions welcome.
On Knockout extenders
You can potentially leverage KO extenders to augment normal observables to translate them on the fly. While this sounds good in theory, I don't think it would actually serve any kind of purpose; you would still need to track every option you pass to the extender, most likely by manually subscribing to each of them and updating the target by calling the wrapped translation function.
If anything, that's merely an alternative syntax, not an alternative approach.
Conclusion
It feels like there is still a lot lacking, but with a 21-lines module I was able to add support for an arbitrary localization library to a standard Durandal application. For an initial time investment, I guess it could be worse. The most difficult part is figuring it out, and I hope I've done a decent job at accelerating that process for you.
In fact, while doing it right may sound a little complicated (well, what I believe is the right way anyway), I'm pretty confident that techniques like these make things globally simpler, at least in comparison to all the trouble you'd get from trying to rebuild state consistently after a document reload or to manually tracking all translated strings without Knockout. Also, it is definitely more efficient (UX can't be smoother): only the strings that need to be retranslated are retranslated and only when necessary.
Nov 2014 notes
After writing this post, we merged the i18next initialization code and the code from the translate module in a single AMD module. This module had an interface that was intended to mimick the rest of the interface of the stock i18next AMD module (though we never got past the translate function), so that the "KO-ification" of the library would be transparent to the applications (except for the fact that it now recognized KO observables and took the locale observable singleton in its configuration, of course). We even managed to reuse the same "i18next" AMD module name with some require.js paths trickery.
So, if you still want to do this integration work, you may rest assured that this is possible, and eventually it seemed like the most sensible solution to us. Keeping the locale observable in a singleton module also turned out to be a good decision.
As for the translation function itself, unwrapping observables using the stock ko.toJS function was indeed far easier.
i18next.js (Knockout integration wrapper)
define(function (require) {
'use strict';
var ko = require('knockout');
var i18next = require('i18next-actual');
var locale = require('locale');
var namespaces = require('tran-namespaces');
var Mutex = require('komutex');
var mutex = new Mutex();
mutex.lock(function (unlock) {
i18next.init({
lng: locale(),
getAsync: true,
fallbackLng: 'en',
resGetPath: 'app/locale/__lng__/__ns__.json',
ns: {
namespaces: namespaces,
defaultNs: namespaces && namespaces[0],
},
}, unlock);
});
locale.subscribe(function (value) {
mutex.lock(function (unlock) {
i18next.setLng(value, unlock);
});
});
var origFn = i18next.t;
i18next.t = i18next.translate = function (key, opts) {
return ko.computed(function () {
return mutex.tryLockAuto(function () {
locale();
return origFn(key, opts && ko.toJS(opts));
});
});
};
return i18next;
});
require.js path trickery (OK, not that tricky)
requirejs.config({
paths: {
'i18next-actual': 'path/to/real/i18next.amd-x.y.z',
'i18next': 'path/to/wrapper/above',
}
});
The locale module is the same singleton presented above, the tran-namespaces module is another singleton that contains the list of i18next namespaces. These singletons are extremely handy not only because they provide a very declarative way of configuring these things, but also because it allows the i18next wrapper (this module) to be entirely self-initialized. In other words, user modules that require it will never have to call init.
Now, initialization takes time (might need to fetch some translation files), and as I already mentioned a year ago, we actually used the async interface (getAsync: true). This means that a user module that calls translate might in fact not get the translation directly (if it asks for a translation before initialization is finished, or when switching locales). Remember, in our implementation user modules can just start calling i18next.t immediately without waiting for a signal from the init callback explicitly; they don't have to call it, and thus we don't even provide a wrapper for this function in our module.
How is this possible? Well, to keep track of all this, we use a "Mutex" object that merely holds a boolean observable. Whenever that mutex is "locked", it means we're initializing or changing locales, and translations shouldn't go through. The state of that mutex is automatically tracked in the translate function by the KO computed observable function that represents the (future) translation and will thus be re-executed automatically (thanks to the magic of KO) when it changes to "unlocked", whereupon the real translate function can retry and do its work.
It's probably more difficult to explain than it is to actually understand (as you can see, the code above is not overly long), feel free to ask for clarifications.
Usage is very easy though; just var i18next = require('i18next') in any module of your application, then call i18next.t away at any time. Just like the initial translate function you may pass observable as arguments (which has the effect of retranslating that particular string automatically every time such an argument is changed) and it will return an observable string. In fact, the function doesn't use this, so you may safely assign it to a convenient variable: var _ = i18next.t.
By now you might be looking up komutex on your favorite search engine. Well, unless somebody had the same idea, you won't find anything, and I don't intend to publish that code as it is (I couldn't do that without losing all my credibility ;)). The explanation above should contain all you need to know to implement the same kind of thing without this module, though it clutters the code with concerns I'm personally inclined to extract in dedicated components as I did here. Toward the end, we weren't even 100% sure that the mutex abstraction was the right one, so even though it might look neat and simple, I advise that you put some thoughts into how to extract that code (or simply on whether to extract it or not).
More generally, I'd also advise you to seek other accounts of such integration work, as its unclear whether these ideas will age well (a year later, I still believe this "reactive" approach to localization/translation is absolutely the right one, but that's just me). Maybe you'll even find more modern libraries that do what you need them to do out of the box.
In any case, it's highly unlikely that I'll revisit this post again. Again, I hope this little(!) update is as useful as the initial post seems to be.
Have fun!
I was quite inspired by the answers in SO regarding this topic, so I came up with my own implementation of a i18n module + binding for Knockout/Durandal.
Take a look at my github repo
The choice for yet another i18n module was that I prefer storing translations in databases (which ever type required per project) instead of files. With that implementation you simply have a backend which has to reply with a JSON object containing all your translations in a key-value manner.
#RainerAtSpirit
Good tip with the singleton class was very helpful for the module
You might consider having one i18n module that returns a singleton with all required observables. In addition a init function that takes an i18n object to initialize/update them.
define(function (require) {
var app = require('durandal/app'),
i18n = require('i18n'),
router = require('durandal/plugins/router')
};
return{
canDeactivate: function () {
}
}
});
On the view:
<label for="hello-world" data-bind="text: i18n.helloWorldLabelText"></label>
Here is an example repo made using i18next, Knockout.Punches, and Knockout 3 with Durandal:
https://github.com/bestguy/knockout3-durandal-i18n
This allows for Handlebars/Angular-style embeds of localized text via an i18n text filter backed by i18next:
<p>
{{ 'home.label' | i18n }}
</p>
also supports attribute embeds:
<h2 title="{{ 'home.title' | i18n }}">
{{ 'home.label' | i18n }}
</h2>
And also lets you pass parameters:
<h2>
{{ 'home.welcome' | i18n:name }}
<!-- Passing the 'name' observable, will be embedded in text string -->
</h2>
JSON example:
English (en):
{
"home": {
"label": "Home Page",
"title": "Type your name…"
"welcome": "Hello {{0}}!",
}
}
Chinese (zh):
{
"home": {
"label": "家",
"title": "输入你的名字……",
"welcome": "{{0}}您好!",
}
}

Categories

Resources