Translating Nested Schema Fields in Meteor’s Autoform

After adding i18n to a single schema / collection via autoforms, the next step would be to manage multiple and nested schemas in enterprise architectures. By the end of this segment, there will be a Schema for an entity called “Locations” with a state and city property that will appear as an array in the Clubs autoform (with internationalization). We will maintain the simulated french set of translations, but continue to use english. If you are starting from this point and don’t want to go through the previous tutorial, you can begin from where we left off by entering the following from a command prompt.

git clone [email protected]:cshanejennings/i18n-autoform-example.git
cd i18n-autoform-example
git checkout 778b07b

Start with the translations for the Locations Schema

Create a file in ./lib/i18n/ named schema.locations.en.i18n.json. significantly sized application and need to organize your data. In this file, we’re going to create a node in the json object called “schema-locations”, just like the others, the node name here is arbitrary. in ./lib/i18n/schema.locations.en.i18n.json

{
    "schema-locations": {
        "state": {
            "label": "en:State"
        },
        "city": {
            "label": "en:City"
        }
    }
}

in ./lib/i18n/schema.locations.fr.i18n.json

{
    "schema-locations": {
        "state": {
            "label": "fr:State"
        },
        "city": {
            "label": "fr:City"
        }
    }
}

You might want to follow the official example for simpler applications

The autoform schema example uses a node called “schemas” and then sub-nodes for each schema. Per the example, you would attach the translations using dot notation like so:

//This code is to illustrate the example from the official plugin and will not be used in our sample application
{
  "schemas": {
    "posts": {
      "title": {
        "label": "Title",
        "placeholder": "Enter a title for your post"
      },
      "category": {
        "label": "Category"
      }
    }
  }
}

MyPostSchema.i18n("schemas.posts");
MyCategorySchema.i18n("schemas.category");

While this is fine for small applications, I think it’s probably more manageable to break the schemas up into multiple files for a significantly sized application where you need to compartmentalize your data. With this reason in mind, we’re going to create the translations for our nested schema in it’s own file and keep everything flat.

Next Create the Locations Schema

Back in ./lib/common.js, add the following to the top of the file. (It’s important we create the Locations schema before we use it in Clubs)

SimpleSchema.LocationsSchema = new SimpleSchema({
  state: {
    type: String,
    max: 12
  },
  city: {
    type: String,
    optional: true
  }
});

SimpleSchema.LocationsSchema.i18n("schema-locations");

Here we created a schema referenced by SimpleSchema.LocationsSchema and gave it a name and city field. We then attached the “schema-locations” node of our translations json dictionary we just created.

Add array of locations to the Clubs Schema

In our example, we have a collection of “clubs” such as hiking, knitting, drone flyers, etc that can have multiple locations, in this case city and state determine the location. Since we created the location schema and now need an array, we array notation in the type node and place our reference to the locations schema here, in this case type: [SimpleSchema.LocationsSchema] as seen below under the new locations node. Change the ClubSchema definition in ./lib/common.js to match the following:

SimpleSchema.ClubSchema = new SimpleSchema({
  name: {
     type: String,
     max: 30,
     optional: false
  },
  locations: {
      type: [SimpleSchema.LocationsSchema],
      minCount: 2,
      maxCount: 6
  }
});

Watch out for this gotcha

The autoform schema example suggests wrapping the schema attachment in a Meteor.startup function like so:

Meteor.startup(function () {
    SimpleSchema.LocationsSchema.i18n("schema-locations");
    SimpleSchema.ClubSchema.i18n("schema-clubs");
});

I’ve found that when I do this, autoform doesn’t seem to translate the field names. I’ve also noticed that fields in arrays of schemas seem to be handled differently than fields in the top level of the schema and I’ve run into situations (that I can’t explain) where the nested schema fields fail translation. This seems to be due to the way autoform handles arrays, so I may cover this in more detail in another segment.

Nested fields are working!

After running meteor, the screen displays as follows, showing the appropriately translated fields. translated-autoform-schema-array

translate-this-form

Meteor internationalization – translating autoform fields

After getting started as outlined in the last post about internationalization, want to cover how I applied i18n to autoforms. By the end of this segment, there will be a Schema for an entity called “Clubs” with only a name property that will display in an internationalized autoform. For now we will only be displaying english, though we will create a simulated french file for use later. If you are starting from this point and don’t want to go through the previous tutorial, you can begin from where we left off by entering the following from a command prompt.

git clone [email protected]:cshanejennings/i18n-autoform-example.git
cd i18n-autoform-example
git checkout 75c8587

Installing SimpleSchema, Autoform and it’s internationalization packages to meteor

To create a Schema, use autoform and apply internationalization to it, you’ll need to install the following packages by running the following from the command prompt:

  • meteor add aldeed:autoform
  • meteor add aldeed:collection2
  • meteor add aldeed:simple-schema
  • meteor add gwendall:autoform-i18n

Creating the internationalization text for the form fields

Going to start by creating the internationalization text we want to see by adding a node called “schema-clubs” to our international json files. This can be spread over multiple files (which I will show in a future segment) or placed in the same file. The convention is to have a single node per schema where each key in the JSON matches a key in the schema. This node can have a label, placeholder and option field (for select boxes). The field options are explained in more detail here. For now, we’re just going to add a label for a name field. To do that…

modify app-root/lib/i18n/en.i18n.json to include the json below:

{
    "title": "en:title english",
    "schema-clubs": {
        "name": {
            "label": "en:Event Name"
        }
    }
}

do the same thing in app-root/lib/i18n/fr.i18n.json

{
    "title": "fr:title english",
    "schema-clubs": {
        "name": {
            "label": "fr:Event Name"
        }
    }
}

Creating the “Club” Schema

Create a new file in lib called common.js and put the schema creation text in that file. The new file will go here app-root/lib/common.js and for now put the following code.

SimpleSchema.ClubSchema = new SimpleSchema({
  name: {
     type: String,
     max: 30,
     optional: false
  }
});
SimpleSchema.ClubSchema.i18n("schema-clubs");

The schema is now being stored as a node called “ClubSchema” on the SimpleSchema global. I’m storing it in a variable because I’ll need to reference it on a few occasions and the variable could really be called anything and not stored on SimpleSchema, I’m just storing it there to keep my options open if I want to reference this schema from another file (this might not be best practice).

Notice I did not add a “label” field to the name field, this is because I want to override the label via the gwendall:autoform-i18n plugin, which will only render in the absence of a label field. The last line is in the form of SchemaVariable.i18n(“schema-node-name”) and applies internationalization to the schema per the gwendall:autoform-i18n plugin.

Create the Collection Using the Schema

In the same file after the above, enter the following code:

Clubs = new Mongo.Collection('clubs');
Clubs.attachSchema(SimpleSchema.ClubSchema);

Once I make the schema, I apply it to a collection I call “Clubs” and then attach SimpleSchema.ClubSchema to it. This will allow autoform to apply the schema to displaying forms.

Set broad permissions on the collection (irresponsibly)

Next I went ahead and completely opened up the permissions as wide as I could since this was a demo app and I wouldn’t have to worry about playing with the files to make it work. This is not an article about security, but I don’t want to use the insecure or autopublish plugins for this example.

Clubs.allow({
  insert: function () { return true; },
  update: function () { return true; },
  remove: function () { return true; }
});

Add the autoform to the main page

To make it pretty, start by adding the twitter bootstrap package by running the following from the command prompt:

meteor add twbs:bootstrap

Next, modify app-root/client/main.html to include the following:

<head>
  <title>Leaderboard</title>
  <meta name="viewport" content="width=device-width, user-scalable=no">
</head>
<body>
    <div class="container">
        <h1>{{_ "title"}}</h1>
        <hr>
        {{#autoForm collection="Clubs" id="createEventForm" type="insert"}}
          {{> afQuickField template="bootstrap3-horizontal" label-class="col-sm-3" input-col-class="col-sm-9" name='name'}}
        {{/autoForm}}
    </div><!-- .container -->
</body>

And I have a template displaying a the same title as before with a single field form underneath with a label of “Name”, inherited from the node name in the schema. Autoform has a host of templating options, I used one of the default templates. Read about autoform templates.

Now I can see the translated title as well as the translated field for the club name. translated-autoform

Next I moved on to nested schemas.

translate-this

Meteor internationalization part 1

Been working with Meteor for my new job at Konduko and given all the mistakes I’ve learned from in the past, I opted to tackle internationalization first. Meteor has a package for i18n that is easily installed, I’m going to document what I find as I work my way towards a more complex use case with forms and nested schema’s, etc. Everything in this post is mirrored in a github repository here, under the first commit.

Creating the file structure

Skipping the explanations for a second, I got the basics working by creating the following files and folders:

.
+-- project-tap.i18n
+-- client
|   +-- app.js
|   +-- main.html
+-- lib
|   +-- i18n
|   |    +-- en.i18n.json
|   |    +-- fr.i18n.json
+-- common.js
+-- server
|   +-- app.js

Installing the packages

For this stage, I only needed the tap:i18n plugin which I installed by running the following from the command prompt in the meteor directory.

meteor add tap:i18n

Configuring tap:i18n

Just for the sake of being thorough, I added the following file to app-root/project-tap.i18n:

{
    "helper_name": "_",
    "supported_languages": null,
    "i18n_files_route": "/tap-i18n",
    "cdn_path": null,
    "preloaded_langs": []
}

This configuration file just explicitly assigns the defaults, but I’m leaving it here in case I want to change anything. A list of the parameters and their functions can be found here

Creating the translation files

The translation files are just json files that I place in the lib/i18n/ folder and follow the convention of *.[language].i18n.json The json files of the same language appear to be combined when the app is initialized, so be careful to avoid overwriting your nodes if you use more than one file. I’ll cover this in more detail in a future post. For now I’m just going to use a title in french and english. (using a prefix as a visual cue that the language has changed).

in app-root/lib/i18n/en.i18n.json

{
    "title": "en:title english"
}

and in app-root/lib/i18n/en.i18n.json

{
    "title": "fr:title english"
}

Creating the template and site html

and in app-root/client/main.html

<head>
  <title>Internationalization Test</title>
  <meta name="viewport" content="width=device-width, user-scalable=no">
</head>
<body>
    <div class="container">
        <h1>{{_ "title"}}</h1>
    </div><!-- .container -->
</body>

The {{_ “title”}} token begins with an underscore, followed by the name of the node in the json. You might recognize the underscore from the project-tap.i18n file where the “helper_name” was set to “_”. After running meteor from the command prompt, this works as expected and I get “en:title english” on the home page in an H1 tag. Next is how to translate auto form fields.

primitive-object-extension

Overlaying Javascript Objects, Keeping it Simple

As mentioned in a previous post, there is often a need to “overlay” values from one javascript object over another. There are lots of ways to do this, this is one of them and light exploration of the concept. In jQuery, you could just call $.extend({}, default_config, site_config, page_config); to get back this object, but you don’t always want an 800 lb gorilla to do something so small. I played a bit of code golf to create an option for mapping an arbitrary number of elements onto another one. It’s important to note, that while this will overlay parameters, it will not perform what is called a “deep_copy”, I’ll go over what’s necessary to make that happen next.

Shallow copy function (Copy this if you just want the code)
 function shallow_extend(){ var args = [].slice.call(arguments, 0).reverse(), obj = args.pop(); function map(n_obj, name) { for (name in n_obj) { obj[name] = n_obj[name]; }} while (args.length) { map(args.pop()); } return obj; } 

This function takes an arbitrary number of arguments as objects, loops through them and progressively overlays their values as it makes its way towards the end.

managing-configuration-tokens

Templating and extending via configuration tokens

Been working on an embed code generator for our Software as a service video players. We are moving from a static embed code to a dynamic one and I’m trying to make sure all the bases are covered so we are more or less “future proof”. To reach a balance of power and usability, I need a robust set of defaults with tiers of configuration to allow for default behavior according to context.

For instance if a customer wants single video players to use a particular style on one site, and another style on their other site while both share the same api token, that would be an example of contextual mapping. The idea being that there are company wide defaults, customer based defaults and n-tiers of customer based defaults, such as site based, folder based, whatever, customers can be astonishingly clever in ways you’d never expect, so we have to be many steps ahead.

It’s pretty common in js architectures that you will need to map one object over another. Say for instance you have some defaults like so:

var default_config = {
        api_v: "4.0"
    },
    customer_config = {
        player_id: "xxxxxxx",
        api_token: "xxxx123457890xxxx"
    },
    site_config = {
        player_id: "yyyyyyyy",
        width: 640,
        height: 360
    },
    page_config = {
        player_id: "zzzzzzz"
    };

That you would like to result in the object:

var result_config = {
    player_id: "zzzzzzzz",
    api_token: "xxxx123457890xxxx",
    api_v: "4.0",
    width: 640,
    height: 360
};

This allows for you to maintain ease of use in different contexts. The general rule in most of the code I write at least, is you want the context to handle as much of the heavy lifting as possible. In the case above, the api version is set as a default for all customers, the individual customer defaults use a particular player id and api token if nothing is specified, the site can place another tier of configuration allowing for a site-specific player as well as dimensions, and finally a particular page could override the player id yet again. As over prepared as this seems, I’ve seen this happen many times.

This ends up being very similar to a specificity order in css and if managed properly can become quite powerful for context based configuration.

On being headspaced

Got a little spontaneous today and registered a domain. Thinking about writing an app with the intentions of exploring and logging a user’s mental state based on command line entries. I’ve been trying to organize some recent thoughts about the collection and use of ideas for a while and I’m frustrated from the internal repetition so it’s time to write some things down. Welcome to headspaced.

Continue reading