diff --git a/.env.dev b/.env.dev
index 2e883414365a1c83a982887c70bb272d04398760..3f904078c36d0a5fa1392356d83a5e1e0d493204 100644
--- a/.env.dev
+++ b/.env.dev
@@ -1,9 +1,11 @@
 API_AUTHENTICATION_REQUIRED=True
 RAVEN_ENABLED=false
 RAVEN_DSN=https://44332e9fdd3d42879c7d35bf8562c6a4:0062dc16a22b41679cd5765e5342f716@sentry.eliotberriot.com/5
-DJANGO_ALLOWED_HOSTS=localhost,nginx
+DJANGO_ALLOWED_HOSTS=.funkwhale.test,localhost,nginx,0.0.0.0,127.0.0.1
 DJANGO_SETTINGS_MODULE=config.settings.local
 DJANGO_SECRET_KEY=dev
 C_FORCE_ROOT=true
-FUNKWHALE_URL=http://localhost
+FUNKWHALE_HOSTNAME=localhost
+FUNKWHALE_PROTOCOL=http
 PYTHONDONTWRITEBYTECODE=true
+WEBPACK_DEVSERVER_PORT=8080
diff --git a/.gitignore b/.gitignore
index 8b511703444291a612645e398d5b568f6e717a6a..548cfd7b3753f07e48f7004df68f9138fa1e5976 100644
--- a/.gitignore
+++ b/.gitignore
@@ -35,7 +35,6 @@ htmlcov
 
 # Translations
 *.mo
-*.pot
 
 # Pycharm
 .idea
@@ -75,6 +74,7 @@ api/static
 api/.pytest_cache
 
 # Front
+front/static/translations
 front/node_modules/
 front/dist/
 front/npm-debug.log*
@@ -87,3 +87,5 @@ docs/_build
 
 data/
 .env
+
+po/*.po
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 94b40bed3b7ca63cd52ae3d492b19cd211fef6d6..217047794ba8c2974dd1fa69396674cd11aea726 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -68,6 +68,8 @@ build_front:
 
   script:
     - yarn install
+    - yarn run i18n-extract
+    - yarn run i18n-compile
     - yarn run build
   cache:
     key: "$CI_PROJECT_ID__front_dependencies"
diff --git a/CHANGELOG b/CHANGELOG
index 0f01b5825169c961c95a0b44ffd3787071ecec0f..7cd7714af4aae93118c5790009a35c54e00f809f 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -3,6 +3,79 @@ Changelog
 
 .. towncrier
 
+0.9 (2018-04-17)
+----------------
+
+Features:
+
+- Add internationalization support (#5)
+- Can now follow and import music from remote libraries (#136, #137)
+
+
+Enhancements:
+
+- Added a i18n-extract yarn script to extract strings to PO files (#162)
+- User admin now includes signup and last login dates (#148)
+- We now use a proper user agent including instance version and url during
+  outgoing requests
+
+
+Federation is here!
+^^^^^^^^^^^^^^^^^^^
+
+This is for real this time, and includes:
+
+- Following other Funkwhale libraries
+- Importing tracks from remote libraries (tracks are hotlinked, and only cached for a short amount of time)
+- Searching accross federated catalogs
+
+Note that by default, federation is opt-in, on a per-instance basis:
+instances will request access to your catalog, and you can accept or refuse
+those requests. You can also revoke the access at any time.
+
+Documentation was updated with relevant instructions to use and benefit
+from this new feature: https://docs.funkwhale.audio/federation.html
+
+Preparing internationalization
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+Funkwhale's front-end as always been english-only, and this is a barrier
+to new users. The work make Funkwhale's interface translatable was started
+in this release by Baptiste. Although nothing is translated yet,
+this release includes behind the stage changes that will make it possible in
+the near future.
+
+Many thank to Baptiste for the hard work and for figuring out a proper solution
+to this difficult problem.
+
+Upgrade path
+^^^^^^^^^^^^
+
+In addition to the usual instructions from
+https://docs.funkwhale.audio/upgrading.html, non-docker users will have
+to setup an additional systemd unit file for recurrent tasks.
+
+This was forgotten in the deployment documentation, but recurrent tasks,
+managed by the celery beat process, will be needed more and more in subsequent
+releases. Right now, we'll be using to clear the cache for federated music files
+and keep disk usage to a minimum.
+
+In the future, they will also be needed to refetch music metadata or federated
+information periodically.
+
+Celery beat can be enabled easily::
+
+    curl -L -o "/etc/systemd/system/funkwhale-beat.service" "https://code.eliotberriot.com/funkwhale/funkwhale/raw/develop/deploy/funkwhale-beat.service"
+    # Also edit /etc/systemd/system/funkwhale.target
+    # and ensure the Wants= line contains the following:
+    # Wants=funkwhale-server.service funkwhale-worker.service funkwhale-beat.service
+    nano /etc/systemd/system/funkwhale.target
+    # reload configuration
+    systemctl daemon-reload
+
+Docker users already have celerybeat enabled.
+
+
 0.8 (2018-04-02)
 ----------------
 
@@ -71,27 +144,16 @@ and add the following snippets::
 This will ensure federation endpoints will be reachable in the future.
 You can of course skip this part if you know you will not federate your instance.
 
-A new ``FEDERATION_ENABLED`` env var have also been added to control wether
+A new ``FEDERATION_ENABLED`` env var have also been added to control whether
 federation is enabled or not on the application side. This settings defaults
-to True, which should have no consequencies at the moment, since actual
+to True, which should have no consequences at the moment, since actual
 federation is not implemented and the only available endpoints are for
 testing purposes.
 
 Add ``FEDERATION_ENABLED=false`` to your .env file to disable federation
 on the application side.
 
-The last step involves generating RSA private and public keys for signing
-your instance requests on the federation. This can be done via::
-
-    # on docker setups
-    docker-compose run --rm api python manage.py generate_keys --no-input
-
-    # on non-docker setups
-    source /srv/funkwhale/virtualenv/bin/activate
-    source /srv/funkwhale/load_env
-    python manage.py generate_keys --no-input
-
-To test and troobleshoot federation, we've added a bot account. This bot is available at @test@yourinstancedomain,
+To test and troubleshoot federation, we've added a bot account. This bot is available at @test@yourinstancedomain,
 and sending it "/ping", for example, via Mastodon, should trigger
 a response.
 
diff --git a/README.rst b/README.rst
index 2d5d2011d2f368332458bd567284e2121a4dfca8..8a0ea49320bb8f63115219c462bb49f3c4355d99 100644
--- a/README.rst
+++ b/README.rst
@@ -206,3 +206,91 @@ Typical workflow for a merge request
 6. Push your branch
 7. Create your merge request
 8. Take a step back and enjoy, we're really grateful you did all of this and took the time to contribute!
+
+
+Internationalization
+--------------------
+
+When working on the front-end, any end-user string should be translated
+using either ``<i18next path="yourstring">`` or the ``$t('yourstring')``
+function.
+
+Extraction is done by calling ``yarn run i18n-extract``, which
+will pull all the strings from source files and put them in a PO file.
+
+
+Working with federation locally
+-------------------------------
+
+To achieve that, you'll need:
+
+1. to update your dns resolver to resolve all your .dev hostnames locally
+2. a reverse proxy (such as traefik) to catch those .dev requests and
+   and with https certificate
+3. two instances (or more) running locally, following the regular dev setup
+
+Resolve .dev names locally
+^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+If you use dnsmasq, this is as simple as doing::
+
+    echo "address=/test/172.17.0.1" | sudo tee /etc/dnsmasq.d/test.conf
+    sudo systemctl restart dnsmasq
+
+If you use NetworkManager with dnsmasq integration, use this instead::
+
+    echo "address=/test/172.17.0.1" | sudo tee /etc/NetworkManager/dnsmasq.d/test.conf
+    sudo systemctl restart NetworkManager
+
+Add wildcard certificate to the trusted certificates
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+Simply copy bundled certificates::
+
+    sudo cp docker/ssl/test.crt /usr/local/share/ca-certificates/
+    sudo update-ca-certificates
+
+This certificate is a wildcard for ``*.funkwhale.test``
+
+Run a reverse proxy for your instances
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+
+Create docker network
+^^^^^^^^^^^^^^^^^^^^
+
+Create the federation network::
+
+    docker network create federation
+
+Launch everything
+^^^^^^^^^^^^^^^^^
+
+Launch the traefik proxy::
+
+    docker-compose -f docker/traefik.yml up -d
+
+Then, in separate terminals, you can setup as many different instances as you
+need::
+
+    export COMPOSE_PROJECT_NAME=node2
+    docker-compose -f dev.yml run --rm api python manage.py migrate
+    docker-compose -f dev.yml run --rm api python manage.py createsuperuser
+    docker-compose -f dev.yml up nginx api front nginx api celeryworker
+
+Note that by default, if you don't export the COMPOSE_PROJECT_NAME,
+we will default to node1 as the name of your instance.
+
+Assuming your project name is ``node1``, your server will be reachable
+at ``https://node1.funkwhale.test/``. Not that you'll have to trust
+the SSL Certificate as it's self signed.
+
+When working on federation with traefik, ensure you have this in your ``env``::
+
+    # This will ensure we don't bind any port on the host, and thus enable
+    # multiple instances of funkwhale to be spawned concurrently.
+    WEBPACK_DEVSERVER_PORT_BINDING=
+    # This disable certificate verification
+    EXTERNAL_REQUESTS_VERIFY_SSL=false
+    # this ensure you don't have incorrect urls pointing to http resources
+    FUNKWHALE_PROTOCOL=https
diff --git a/api/config/api_urls.py b/api/config/api_urls.py
index cab6805b67e394838ec942ecf8b162edb08a88cf..cf5b03744d8b12d5a91fd43df4edbad941870389 100644
--- a/api/config/api_urls.py
+++ b/api/config/api_urls.py
@@ -32,6 +32,10 @@ v1_patterns += [
         include(
             ('funkwhale_api.instance.urls', 'instance'),
             namespace='instance')),
+    url(r'^federation/',
+        include(
+            ('funkwhale_api.federation.api_urls', 'federation'),
+            namespace='federation')),
     url(r'^providers/',
         include(
             ('funkwhale_api.providers.urls', 'providers'),
diff --git a/api/config/settings/common.py b/api/config/settings/common.py
index fbe3b7045e24c67d87c2ee90441103ccc27ac57a..a972ec7effc9e5696f94833c19c52096ff307f14 100644
--- a/api/config/settings/common.py
+++ b/api/config/settings/common.py
@@ -13,6 +13,8 @@ from __future__ import absolute_import, unicode_literals
 from urllib.parse import urlsplit
 import os
 import environ
+from celery.schedules import crontab
+
 from funkwhale_api import __version__
 
 ROOT_DIR = environ.Path(__file__) - 3  # (/a/b/myfile.py - 3 = /)
@@ -25,12 +27,35 @@ try:
 except FileNotFoundError:
     pass
 
-FUNKWHALE_URL = env('FUNKWHALE_URL')
-FUNKWHALE_HOSTNAME = urlsplit(FUNKWHALE_URL).netloc
+FUNKWHALE_HOSTNAME = None
+FUNKWHALE_HOSTNAME_SUFFIX = env('FUNKWHALE_HOSTNAME_SUFFIX', default=None)
+FUNKWHALE_HOSTNAME_PREFIX = env('FUNKWHALE_HOSTNAME_PREFIX', default=None)
+if FUNKWHALE_HOSTNAME_PREFIX and FUNKWHALE_HOSTNAME_SUFFIX:
+    # We're in traefik case, in development
+    FUNKWHALE_HOSTNAME = '{}.{}'.format(
+        FUNKWHALE_HOSTNAME_PREFIX, FUNKWHALE_HOSTNAME_SUFFIX)
+    FUNKWHALE_PROTOCOL = env('FUNKWHALE_PROTOCOL', default='https')
+else:
+    try:
+        FUNKWHALE_HOSTNAME = env('FUNKWHALE_HOSTNAME')
+        FUNKWHALE_PROTOCOL = env('FUNKWHALE_PROTOCOL', default='https')
+    except Exception:
+        FUNKWHALE_URL = env('FUNKWHALE_URL')
+        _parsed = urlsplit(FUNKWHALE_URL)
+        FUNKWHALE_HOSTNAME = _parsed.netloc
+        FUNKWHALE_PROTOCOL = _parsed.scheme
+
+FUNKWHALE_URL = '{}://{}'.format(FUNKWHALE_PROTOCOL, FUNKWHALE_HOSTNAME)
+
 
 FEDERATION_ENABLED = env.bool('FEDERATION_ENABLED', default=True)
 FEDERATION_HOSTNAME = env('FEDERATION_HOSTNAME', default=FUNKWHALE_HOSTNAME)
-
+FEDERATION_COLLECTION_PAGE_SIZE = env.int(
+    'FEDERATION_COLLECTION_PAGE_SIZE', default=50
+)
+FEDERATION_MUSIC_NEEDS_APPROVAL = env.bool(
+    'FEDERATION_MUSIC_NEEDS_APPROVAL', default=True
+)
 ALLOWED_HOSTS = env.list('DJANGO_ALLOWED_HOSTS')
 
 # APP CONFIGURATION
@@ -144,16 +169,6 @@ FIXTURE_DIRS = (
 # ------------------------------------------------------------------------------
 EMAIL_BACKEND = env('DJANGO_EMAIL_BACKEND', default='django.core.mail.backends.smtp.EmailBackend')
 
-# MANAGER CONFIGURATION
-# ------------------------------------------------------------------------------
-# See: https://docs.djangoproject.com/en/dev/ref/settings/#admins
-ADMINS = (
-    ("""Eliot Berriot""", 'contact@eliotberriot.om'),
-)
-
-# See: https://docs.djangoproject.com/en/dev/ref/settings/#managers
-MANAGERS = ADMINS
-
 # DATABASE CONFIGURATION
 # ------------------------------------------------------------------------------
 # See: https://docs.djangoproject.com/en/dev/ref/settings/#databases
@@ -321,6 +336,16 @@ CELERY_BROKER_URL = env(
 # Your common stuff: Below this line define 3rd party library settings
 CELERY_TASK_DEFAULT_RATE_LIMIT = 1
 CELERY_TASK_TIME_LIMIT = 300
+CELERYBEAT_SCHEDULE = {
+    'federation.clean_music_cache': {
+        'task': 'funkwhale_api.federation.tasks.clean_music_cache',
+        'schedule': crontab(hour='*/2'),
+        'options': {
+            'expires': 60 * 2,
+        },
+    }
+}
+
 import datetime
 JWT_AUTH = {
     'JWT_ALLOW_REFRESH': True,
@@ -411,3 +436,8 @@ ACCOUNT_USERNAME_BLACKLIST = [
     'staff',
     'service',
 ] + env.list('ACCOUNT_USERNAME_BLACKLIST', default=[])
+
+EXTERNAL_REQUESTS_VERIFY_SSL = env.bool(
+    'EXTERNAL_REQUESTS_VERIFY_SSL',
+    default=True
+)
diff --git a/api/funkwhale_api/__init__.py b/api/funkwhale_api/__init__.py
index 0a12b794efb05892fa6fca0bff82b192e936f108..70cf5b1f85e8b784a35f886a4f6f64a8ac54e5c3 100644
--- a/api/funkwhale_api/__init__.py
+++ b/api/funkwhale_api/__init__.py
@@ -1,3 +1,3 @@
 # -*- coding: utf-8 -*-
-__version__ = '0.7'
+__version__ = '0.9'
 __version_info__ = tuple([int(num) if num.isdigit() else num for num in __version__.replace('-', '.', 1).split('.')])
diff --git a/api/funkwhale_api/common/fields.py b/api/funkwhale_api/common/fields.py
index 1a18b5f27d1e1839ef32722499de6b4365d6ac55..98e971662758ed67959431beead7052c34b43e43 100644
--- a/api/funkwhale_api/common/fields.py
+++ b/api/funkwhale_api/common/fields.py
@@ -1,5 +1,9 @@
+import django_filters
+
 from django.db import models
 
+from funkwhale_api.music import utils
+
 
 PRIVACY_LEVEL_CHOICES = [
     ('me', 'Only me'),
@@ -25,3 +29,15 @@ def privacy_level_query(user, lookup_field='privacy_level'):
             'followers', 'instance', 'everyone'
         ]
     })
+
+
+class SearchFilter(django_filters.CharFilter):
+    def __init__(self, *args, **kwargs):
+        self.search_fields = kwargs.pop('search_fields')
+        super().__init__(*args, **kwargs)
+
+    def filter(self, qs, value):
+        if not value:
+            return qs
+        query = utils.get_query(value, self.search_fields)
+        return qs.filter(query)
diff --git a/api/funkwhale_api/common/session.py b/api/funkwhale_api/common/session.py
new file mode 100644
index 0000000000000000000000000000000000000000..7f5584bd1cdbe465605d719d164ba468024160ed
--- /dev/null
+++ b/api/funkwhale_api/common/session.py
@@ -0,0 +1,18 @@
+import requests
+
+from django.conf import settings
+
+import funkwhale_api
+
+
+def get_user_agent():
+    return 'python-requests (funkwhale/{}; +{})'.format(
+        funkwhale_api.__version__,
+        settings.FUNKWHALE_URL
+    )
+
+
+def get_session():
+    s = requests.Session()
+    s.headers['User-Agent'] = get_user_agent()
+    return s
diff --git a/api/funkwhale_api/common/utils.py b/api/funkwhale_api/common/utils.py
index c9d450e6ad7c9c80c9682be95a487d84263625c4..2d7641bf56c077ae08384a92759008f48d06a7f4 100644
--- a/api/funkwhale_api/common/utils.py
+++ b/api/funkwhale_api/common/utils.py
@@ -1,3 +1,4 @@
+from urllib.parse import urlencode, parse_qs, urlsplit, urlunsplit
 import os
 import shutil
 
@@ -25,3 +26,20 @@ def on_commit(f, *args, **kwargs):
     return transaction.on_commit(
         lambda: f(*args, **kwargs)
     )
+
+
+def set_query_parameter(url, **kwargs):
+    """Given a URL, set or replace a query parameter and return the
+    modified URL.
+
+    >>> set_query_parameter('http://example.com?foo=bar&biz=baz', 'foo', 'stuff')
+    'http://example.com?foo=stuff&biz=baz'
+    """
+    scheme, netloc, path, query_string, fragment = urlsplit(url)
+    query_params = parse_qs(query_string)
+
+    for param_name, param_value in kwargs.items():
+        query_params[param_name] = [param_value]
+    new_query_string = urlencode(query_params, doseq=True)
+
+    return urlunsplit((scheme, netloc, path, new_query_string, fragment))
diff --git a/api/funkwhale_api/downloader/downloader.py b/api/funkwhale_api/downloader/downloader.py
index b35ed6f7d1c73ab99b9d0b0676e77743c80c10d3..7fc237b089a748ccb9555da9a52e0eaf1c506a4e 100644
--- a/api/funkwhale_api/downloader/downloader.py
+++ b/api/funkwhale_api/downloader/downloader.py
@@ -1,5 +1,4 @@
 import os
-import requests
 import json
 from urllib.parse import quote_plus
 import youtube_dl
diff --git a/api/funkwhale_api/federation/activity.py b/api/funkwhale_api/federation/activity.py
index 4eeb193b183007ad6c7c093cd20b881a3a06b89b..becf6c96f55b68d2503eb7442f673e0fadb4cef7 100644
--- a/api/funkwhale_api/federation/activity.py
+++ b/api/funkwhale_api/federation/activity.py
@@ -1,11 +1,5 @@
-import logging
-import json
-import requests
-import requests_http_signature
-
-from . import signing
-
-logger = logging.getLogger(__name__)
+from . import serializers
+from . import tasks
 
 ACTIVITY_TYPES = [
     'Accept',
@@ -42,44 +36,32 @@ ACTIVITY_TYPES = [
 OBJECT_TYPES = [
     'Article',
     'Audio',
+    'Collection',
     'Document',
     'Event',
     'Image',
     'Note',
+    'OrderedCollection',
     'Page',
     'Place',
     'Profile',
     'Relationship',
     'Tombstone',
     'Video',
-]
+] + ACTIVITY_TYPES
+
 
 def deliver(activity, on_behalf_of, to=[]):
-    from . import actors
-    logger.info('Preparing activity delivery to %s', to)
-    auth = requests_http_signature.HTTPSignatureAuth(
-        use_auth_header=False,
-        headers=[
-            '(request-target)',
-            'user-agent',
-            'host',
-            'date',
-            'content-type',],
-        algorithm='rsa-sha256',
-        key=on_behalf_of.private_key.encode('utf-8'),
-        key_id=on_behalf_of.private_key_id,
+    return tasks.send.delay(
+        activity=activity,
+        actor_id=on_behalf_of.pk,
+        to=to
     )
-    for url in to:
-        recipient_actor = actors.get_actor(url)
-        logger.debug('delivering to %s', recipient_actor.inbox_url)
-        logger.debug('activity content: %s', json.dumps(activity))
-        response = requests.post(
-            auth=auth,
-            json=activity,
-            url=recipient_actor.inbox_url,
-            headers={
-                'Content-Type': 'application/activity+json'
-            }
-        )
-        response.raise_for_status()
-        logger.debug('Remote answered with %s', response.status_code)
+
+
+def accept_follow(follow):
+    serializer = serializers.AcceptFollowSerializer(follow)
+    return deliver(
+        serializer.data,
+        to=[follow.actor.url],
+        on_behalf_of=follow.target)
diff --git a/api/funkwhale_api/federation/actors.py b/api/funkwhale_api/federation/actors.py
index 69033f5ca890cb094f63d5dbc5bf7f39afe3de44..380bb23c01e38e0651e3a3fc7ee0244025adcf34 100644
--- a/api/funkwhale_api/federation/actors.py
+++ b/api/funkwhale_api/federation/actors.py
@@ -1,8 +1,9 @@
 import logging
-import requests
+import uuid
 import xml
 
 from django.conf import settings
+from django.db import transaction
 from django.urls import reverse
 from django.utils import timezone
 
@@ -10,9 +11,16 @@ from rest_framework.exceptions import PermissionDenied
 
 from dynamic_preferences.registries import global_preferences_registry
 
+from funkwhale_api.common import session
+from funkwhale_api.common import utils as funkwhale_utils
+from funkwhale_api.music import models as music_models
+from funkwhale_api.music import tasks as music_tasks
+
 from . import activity
+from . import keys
 from . import models
 from . import serializers
+from . import signing
 from . import utils
 
 logger = logging.getLogger(__name__)
@@ -24,8 +32,10 @@ def remove_tags(text):
 
 
 def get_actor_data(actor_url):
-    response = requests.get(
+    response = session.get_session().get(
         actor_url,
+        timeout=5,
+        verify=settings.EXTERNAL_REQUESTS_VERIFY_SSL,
         headers={
             'Accept': 'application/activity+json',
         }
@@ -37,6 +47,7 @@ def get_actor_data(actor_url):
         raise ValueError(
             'Invalid actor payload: {}'.format(response.text))
 
+
 def get_actor(actor_url):
     data = get_actor_data(actor_url)
     serializer = serializers.ActorSerializer(data=data)
@@ -47,31 +58,48 @@ def get_actor(actor_url):
 
 class SystemActor(object):
     additional_attributes = {}
+    manually_approves_followers = False
+
+    def get_request_auth(self):
+        actor = self.get_actor_instance()
+        return signing.get_auth(
+            actor.private_key, actor.private_key_id)
+
+    def serialize(self):
+        actor = self.get_actor_instance()
+        serializer = serializers.ActorSerializer(actor)
+        return serializer.data
 
     def get_actor_instance(self):
-        a = models.Actor(
-            **self.get_instance_argument(
-                self.id,
-                name=self.name,
-                summary=self.summary,
-                **self.additional_attributes
-            )
+        try:
+            return models.Actor.objects.get(url=self.get_actor_url())
+        except models.Actor.DoesNotExist:
+            pass
+        private, public = keys.get_key_pair()
+        args = self.get_instance_argument(
+            self.id,
+            name=self.name,
+            summary=self.summary,
+            **self.additional_attributes
         )
-        a.pk = self.id
-        return a
+        args['private_key'] = private.decode('utf-8')
+        args['public_key'] = public.decode('utf-8')
+        return models.Actor.objects.create(**args)
+
+    def get_actor_url(self):
+        return utils.full_url(
+            reverse(
+                'federation:instance-actors-detail',
+                kwargs={'actor': self.id}))
 
     def get_instance_argument(self, id, name, summary, **kwargs):
-        preferences = global_preferences_registry.manager()
         p = {
             'preferred_username': id,
             'domain': settings.FEDERATION_HOSTNAME,
             'type': 'Person',
             'name': name.format(host=settings.FEDERATION_HOSTNAME),
             'manually_approves_followers': True,
-            'url': utils.full_url(
-                reverse(
-                    'federation:instance-actors-detail',
-                    kwargs={'actor': id})),
+            'url': self.get_actor_url(),
             'shared_inbox_url': utils.full_url(
                 reverse(
                     'federation:instance-actors-inbox',
@@ -84,8 +112,6 @@ class SystemActor(object):
                 reverse(
                     'federation:instance-actors-outbox',
                     kwargs={'actor': id})),
-            'public_key': preferences['federation__public_key'],
-            'private_key': preferences['federation__private_key'],
             'summary': summary.format(host=settings.FEDERATION_HOSTNAME)
         }
         p.update(kwargs)
@@ -95,7 +121,7 @@ class SystemActor(object):
         raise NotImplementedError
 
     def post_inbox(self, data, actor=None):
-        raise NotImplementedError
+        return self.handle(data, actor=actor)
 
     def get_outbox(self, data, actor=None):
         raise NotImplementedError
@@ -103,6 +129,70 @@ class SystemActor(object):
     def post_outbox(self, data, actor=None):
         raise NotImplementedError
 
+    def handle(self, data, actor=None):
+        """
+        Main entrypoint for handling activities posted to the
+        actor's inbox
+        """
+        logger.info('Received activity on %s inbox', self.id)
+
+        if actor is None:
+            raise PermissionDenied('Actor not authenticated')
+
+        serializer = serializers.ActivitySerializer(
+            data=data, context={'actor': actor})
+        serializer.is_valid(raise_exception=True)
+
+        ac = serializer.data
+        try:
+            handler = getattr(
+                self, 'handle_{}'.format(ac['type'].lower()))
+        except (KeyError, AttributeError):
+            logger.debug(
+                'No handler for activity %s', ac['type'])
+            return
+
+        return handler(data, actor)
+
+    def handle_follow(self, ac, sender):
+        system_actor = self.get_actor_instance()
+        serializer = serializers.FollowSerializer(
+            data=ac, context={'follow_actor': sender})
+        if not serializer.is_valid():
+            return logger.info('Invalid follow payload')
+        approved = True if not self.manually_approves_followers else None
+        follow = serializer.save(approved=approved)
+        if follow.approved:
+            return activity.accept_follow(follow)
+
+    def handle_accept(self, ac, sender):
+        system_actor = self.get_actor_instance()
+        serializer = serializers.AcceptFollowSerializer(
+            data=ac,
+            context={'follow_target': sender, 'follow_actor': system_actor})
+        if not serializer.is_valid(raise_exception=True):
+            return logger.info('Received invalid payload')
+
+        return serializer.save()
+
+    def handle_undo_follow(self, ac, sender):
+        system_actor = self.get_actor_instance()
+        serializer = serializers.UndoFollowSerializer(
+            data=ac, context={'actor': sender, 'target': system_actor})
+        if not serializer.is_valid():
+            return logger.info('Received invalid payload')
+        serializer.save()
+
+    def handle_undo(self, ac, sender):
+        if ac['object']['type'] != 'Follow':
+            return
+
+        if ac['object']['actor'] != sender.url:
+            # not the same actor, permission issue
+            return
+
+        self.handle_undo_follow(ac, sender)
+
 
 class LibraryActor(SystemActor):
     id = 'library'
@@ -112,6 +202,84 @@ class LibraryActor(SystemActor):
         'manually_approves_followers': True
     }
 
+    def serialize(self):
+        data = super().serialize()
+        urls = data.setdefault('url', [])
+        urls.append({
+            'type': 'Link',
+            'mediaType': 'application/activity+json',
+            'name': 'library',
+            'href': utils.full_url(reverse('federation:music:files-list'))
+        })
+        return data
+
+    @property
+    def manually_approves_followers(self):
+        return settings.FEDERATION_MUSIC_NEEDS_APPROVAL
+
+    @transaction.atomic
+    def handle_create(self, ac, sender):
+        try:
+            remote_library = models.Library.objects.get(
+                actor=sender,
+                federation_enabled=True,
+            )
+        except models.Library.DoesNotExist:
+            logger.info(
+                'Skipping import, we\'re not following %s', sender.url)
+            return
+
+        if ac['object']['type'] != 'Collection':
+            return
+
+        if ac['object']['totalItems'] <= 0:
+            return
+
+        try:
+            items = ac['object']['items']
+        except KeyError:
+            logger.warning('No items in collection!')
+            return
+
+        item_serializers = [
+            serializers.AudioSerializer(
+                data=i, context={'library': remote_library})
+            for i in items
+        ]
+        now = timezone.now()
+        valid_serializers = []
+        for s in item_serializers:
+            if s.is_valid():
+                valid_serializers.append(s)
+            else:
+                logger.debug(
+                    'Skipping invalid item %s, %s', s.initial_data, s.errors)
+
+        lts = []
+        for s in valid_serializers:
+            lts.append(s.save())
+
+        if remote_library.autoimport:
+            batch = music_models.ImportBatch.objects.create(
+                source='federation',
+            )
+            for lt in lts:
+                if lt.creation_date < now:
+                    # track was already in the library, we do not trigger
+                    # an import
+                    continue
+                job = music_models.ImportJob.objects.create(
+                    batch=batch,
+                    library_track=lt,
+                    mbid=lt.mbid,
+                    source=lt.url,
+                )
+                funkwhale_utils.on_commit(
+                    music_tasks.import_job_run.delay,
+                    import_job_id=job.pk,
+                    use_acoustid=False,
+                )
+
 
 class TestActor(SystemActor):
     id = 'test'
@@ -123,40 +291,24 @@ class TestActor(SystemActor):
     additional_attributes = {
         'manually_approves_followers': False
     }
+    manually_approves_followers = False
 
     def get_outbox(self, data, actor=None):
         return {
-        	"@context": [
-        		"https://www.w3.org/ns/activitystreams",
-        		"https://w3id.org/security/v1",
-        		{}
-        	],
-        	"id": utils.full_url(
+            "@context": [
+                "https://www.w3.org/ns/activitystreams",
+                "https://w3id.org/security/v1",
+                {}
+            ],
+            "id": utils.full_url(
                 reverse(
                     'federation:instance-actors-outbox',
                     kwargs={'actor': self.id})),
-        	"type": "OrderedCollection",
-        	"totalItems": 0,
-        	"orderedItems": []
+            "type": "OrderedCollection",
+            "totalItems": 0,
+            "orderedItems": []
         }
 
-    def post_inbox(self, data, actor=None):
-        if actor is None:
-            raise PermissionDenied('Actor not authenticated')
-
-        serializer = serializers.ActivitySerializer(
-            data=data, context={'actor': actor})
-        serializer.is_valid(raise_exception=True)
-
-        ac = serializer.validated_data
-        logger.info('Received activity on %s inbox', self.id)
-        if ac['type'] == 'Create' and ac['object']['type'] == 'Note':
-            # we received a toot \o/
-            command = self.parse_command(ac['object']['content'])
-            logger.debug('Parsed command: %s', command)
-            if command == 'ping':
-                self.handle_ping(ac, actor)
-
     def parse_command(self, message):
         """
         Remove any links or fancy markup to extract /command from
@@ -168,7 +320,16 @@ class TestActor(SystemActor):
         except IndexError:
             return
 
-    def handle_ping(self, ac, sender):
+    def handle_create(self, ac, sender):
+        if ac['object']['type'] != 'Note':
+            return
+
+        # we received a toot \o/
+        command = self.parse_command(ac['object']['content'])
+        logger.debug('Parsed command: %s', command)
+        if command != 'ping':
+            return
+
         now = timezone.now()
         test_actor = self.get_actor_instance()
         reply_url = 'https://{}/activities/note/{}'.format(
@@ -179,10 +340,10 @@ class TestActor(SystemActor):
         )
         reply_activity = {
             "@context": [
-        		"https://www.w3.org/ns/activitystreams",
-        		"https://w3id.org/security/v1",
-        		{}
-        	],
+                "https://www.w3.org/ns/activitystreams",
+                "https://w3id.org/security/v1",
+                {}
+            ],
             'type': 'Create',
             'actor': test_actor.url,
             'id': '{}/activity'.format(reply_url),
@@ -214,6 +375,39 @@ class TestActor(SystemActor):
             to=[ac['actor']],
             on_behalf_of=test_actor)
 
+    def handle_follow(self, ac, sender):
+        super().handle_follow(ac, sender)
+        # also, we follow back
+        test_actor = self.get_actor_instance()
+        follow_back = models.Follow.objects.get_or_create(
+            actor=test_actor,
+            target=sender,
+            approved=None,
+        )[0]
+        activity.deliver(
+            serializers.FollowSerializer(follow_back).data,
+            to=[follow_back.target.url],
+            on_behalf_of=follow_back.actor)
+
+    def handle_undo_follow(self, ac, sender):
+        super().handle_undo_follow(ac, sender)
+        actor = self.get_actor_instance()
+        # we also unfollow the sender, if possible
+        try:
+            follow = models.Follow.objects.get(
+                target=sender,
+                actor=actor,
+            )
+        except models.Follow.DoesNotExist:
+            return
+        undo = serializers.UndoFollowSerializer(follow).data
+        follow.delete()
+        activity.deliver(
+            undo,
+            to=[sender.url],
+            on_behalf_of=actor)
+
+
 SYSTEM_ACTORS = {
     'library': LibraryActor(),
     'test': TestActor(),
diff --git a/api/funkwhale_api/federation/admin.py b/api/funkwhale_api/federation/admin.py
new file mode 100644
index 0000000000000000000000000000000000000000..6a097174b552658e0f9c1d5075932300f23e3bdd
--- /dev/null
+++ b/api/funkwhale_api/federation/admin.py
@@ -0,0 +1,66 @@
+from django.contrib import admin
+
+from . import models
+
+
+@admin.register(models.Actor)
+class ActorAdmin(admin.ModelAdmin):
+    list_display = [
+        'url',
+        'domain',
+        'preferred_username',
+        'type',
+        'creation_date',
+        'last_fetch_date']
+    search_fields = ['url', 'domain', 'preferred_username']
+    list_filter = [
+        'type'
+    ]
+
+
+@admin.register(models.Follow)
+class FollowAdmin(admin.ModelAdmin):
+    list_display = [
+        'actor',
+        'target',
+        'approved',
+        'creation_date'
+    ]
+    list_filter = [
+        'approved'
+    ]
+    search_fields = ['actor__url', 'target__url']
+    list_select_related = True
+
+
+@admin.register(models.Library)
+class LibraryAdmin(admin.ModelAdmin):
+    list_display = [
+        'actor',
+        'url',
+        'creation_date',
+        'fetched_date',
+        'tracks_count']
+    search_fields = ['actor__url', 'url']
+    list_filter = [
+        'federation_enabled',
+        'download_files',
+        'autoimport',
+    ]
+    list_select_related = True
+
+
+@admin.register(models.LibraryTrack)
+class LibraryTrackAdmin(admin.ModelAdmin):
+    list_display = [
+        'title',
+        'artist_name',
+        'album_title',
+        'url',
+        'library',
+        'creation_date',
+        'published_date',
+    ]
+    search_fields = [
+        'library__url', 'url', 'artist_name', 'title', 'album_title']
+    list_select_related = True
diff --git a/api/funkwhale_api/federation/api_urls.py b/api/funkwhale_api/federation/api_urls.py
new file mode 100644
index 0000000000000000000000000000000000000000..41dd1c0f99e44c4602f3592f0aca292f212b4876
--- /dev/null
+++ b/api/funkwhale_api/federation/api_urls.py
@@ -0,0 +1,15 @@
+from rest_framework import routers
+
+from . import views
+
+router = routers.SimpleRouter()
+router.register(
+    r'libraries',
+    views.LibraryViewSet,
+    'libraries')
+router.register(
+    r'library-tracks',
+    views.LibraryTrackViewSet,
+    'library-tracks')
+
+urlpatterns = router.urls
diff --git a/api/funkwhale_api/federation/authentication.py b/api/funkwhale_api/federation/authentication.py
index e199ef134d03e0d7026ecffbbaaa1f38e8254e02..7f8ad6653995b4a0f3efd72dc82e4303da5d8760 100644
--- a/api/funkwhale_api/federation/authentication.py
+++ b/api/funkwhale_api/federation/authentication.py
@@ -7,6 +7,7 @@ from rest_framework import exceptions
 
 from . import actors
 from . import keys
+from . import models
 from . import serializers
 from . import signing
 from . import utils
@@ -42,11 +43,16 @@ class SignatureAuthentication(authentication.BaseAuthentication):
         except cryptography.exceptions.InvalidSignature:
             raise exceptions.AuthenticationFailed('Invalid signature')
 
-        return serializer.build()
+        try:
+            return models.Actor.objects.get(url=actor_data['id'])
+        except models.Actor.DoesNotExist:
+            return serializer.save()
 
     def authenticate(self, request):
         setattr(request, 'actor', None)
         actor = self.authenticate_actor(request)
+        if not actor:
+            return
         user = AnonymousUser()
         setattr(request, 'actor', actor)
         return (user, None)
diff --git a/api/funkwhale_api/federation/dynamic_preferences_registry.py b/api/funkwhale_api/federation/dynamic_preferences_registry.py
index 83d0285be263d228ffbb564a9bfc9f898fe77dbf..43877c75c9717eda3c46c0f039366803aadd77c9 100644
--- a/api/funkwhale_api/federation/dynamic_preferences_registry.py
+++ b/api/funkwhale_api/federation/dynamic_preferences_registry.py
@@ -7,28 +7,14 @@ federation = types.Section('federation')
 
 
 @global_preferences_registry.register
-class FederationPrivateKey(types.StringPreference):
-    show_in_api = False
+class MusicCacheDuration(types.IntPreference):
+    show_in_api = True
     section = federation
-    name = 'private_key'
-    default = ''
+    name = 'music_cache_duration'
+    default = 60 * 24 * 2
+    verbose_name = 'Music cache duration'
     help_text = (
-        'Instance private key, used for signing federation HTTP requests'
-    )
-    verbose_name = (
-        'Instance private key (keep it secret, do not change it)'
-    )
-
-
-@global_preferences_registry.register
-class FederationPublicKey(types.StringPreference):
-    show_in_api = False
-    section = federation
-    name = 'public_key'
-    default = ''
-    help_text = (
-        'Instance public key, used for signing federation HTTP requests'
-    )
-    verbose_name = (
-        'Instance public key (do not change it)'
+        'How much minutes do you want to keep a copy of federated tracks'
+        'locally? Federated files that were not listened in this interval '
+        'will be erased and refetched from the remote on the next listening.'
     )
diff --git a/api/funkwhale_api/federation/factories.py b/api/funkwhale_api/federation/factories.py
index 88c86f791937cd66fbcc72a42592b520078c8a7c..0754c4b2f44123e743f9df503c4457b529a17584 100644
--- a/api/funkwhale_api/federation/factories.py
+++ b/api/funkwhale_api/federation/factories.py
@@ -1,8 +1,10 @@
 import factory
 import requests
 import requests_http_signature
+import uuid
 
 from django.utils import timezone
+from django.conf import settings
 
 from funkwhale_api.factories import registry
 
@@ -51,9 +53,23 @@ class SignedRequestFactory(factory.Factory):
         self.headers.update(default_headers)
 
 
+@registry.register(name='federation.Link')
+class LinkFactory(factory.Factory):
+    type = 'Link'
+    href = factory.Faker('url')
+    mediaType = 'text/html'
+
+    class Meta:
+        model = dict
+
+    class Params:
+        audio = factory.Trait(
+            mediaType=factory.Iterator(['audio/mp3', 'audio/ogg'])
+        )
+
+
 @registry.register
 class ActorFactory(factory.DjangoModelFactory):
-
     public_key = None
     private_key = None
     preferred_username = factory.Faker('user_name')
@@ -66,6 +82,12 @@ class ActorFactory(factory.DjangoModelFactory):
     class Meta:
         model = models.Actor
 
+    class Params:
+        local = factory.Trait(
+            domain=factory.LazyAttribute(
+                lambda o: settings.FEDERATION_HOSTNAME)
+        )
+
     @classmethod
     def _generate(cls, create, attrs):
         has_public = attrs.get('public_key') is not None
@@ -77,6 +99,98 @@ class ActorFactory(factory.DjangoModelFactory):
         return super()._generate(create, attrs)
 
 
+@registry.register
+class FollowFactory(factory.DjangoModelFactory):
+    target = factory.SubFactory(ActorFactory)
+    actor = factory.SubFactory(ActorFactory)
+
+    class Meta:
+        model = models.Follow
+
+    class Params:
+        local = factory.Trait(
+            actor=factory.SubFactory(ActorFactory, local=True)
+        )
+
+
+@registry.register
+class LibraryFactory(factory.DjangoModelFactory):
+    actor = factory.SubFactory(ActorFactory)
+    url = factory.Faker('url')
+    federation_enabled = True
+    download_files = False
+    autoimport = False
+
+    class Meta:
+        model = models.Library
+
+
+class ArtistMetadataFactory(factory.Factory):
+    name = factory.Faker('name')
+
+    class Meta:
+        model = dict
+
+    class Params:
+        musicbrainz = factory.Trait(
+            musicbrainz_id=factory.Faker('uuid4')
+        )
+
+
+class ReleaseMetadataFactory(factory.Factory):
+    title = factory.Faker('sentence')
+
+    class Meta:
+        model = dict
+
+    class Params:
+        musicbrainz = factory.Trait(
+            musicbrainz_id=factory.Faker('uuid4')
+        )
+
+
+class RecordingMetadataFactory(factory.Factory):
+    title = factory.Faker('sentence')
+
+    class Meta:
+        model = dict
+
+    class Params:
+        musicbrainz = factory.Trait(
+            musicbrainz_id=factory.Faker('uuid4')
+        )
+
+
+@registry.register(name='federation.LibraryTrackMetadata')
+class LibraryTrackMetadataFactory(factory.Factory):
+    artist = factory.SubFactory(ArtistMetadataFactory)
+    recording = factory.SubFactory(RecordingMetadataFactory)
+    release = factory.SubFactory(ReleaseMetadataFactory)
+
+    class Meta:
+        model = dict
+
+
+@registry.register
+class LibraryTrackFactory(factory.DjangoModelFactory):
+    library = factory.SubFactory(LibraryFactory)
+    url = factory.Faker('url')
+    title = factory.Faker('sentence')
+    artist_name = factory.Faker('sentence')
+    album_title = factory.Faker('sentence')
+    audio_url = factory.Faker('url')
+    audio_mimetype = 'audio/ogg'
+    metadata = factory.SubFactory(LibraryTrackMetadataFactory)
+
+    class Meta:
+        model = models.LibraryTrack
+
+    class Params:
+        with_audio_file = factory.Trait(
+            audio_file=factory.django.FileField()
+        )
+
+
 @registry.register(name='federation.Note')
 class NoteFactory(factory.Factory):
     type = 'Note'
@@ -89,3 +203,51 @@ class NoteFactory(factory.Factory):
 
     class Meta:
         model = dict
+
+
+@registry.register(name='federation.Activity')
+class ActivityFactory(factory.Factory):
+    type = 'Create'
+    id = factory.Faker('url')
+    published = factory.LazyFunction(
+        lambda: timezone.now().isoformat()
+    )
+    actor = factory.Faker('url')
+    object = factory.SubFactory(
+        NoteFactory,
+        actor=factory.SelfAttribute('..actor'),
+        published=factory.SelfAttribute('..published'))
+
+    class Meta:
+        model = dict
+
+
+@registry.register(name='federation.AudioMetadata')
+class AudioMetadataFactory(factory.Factory):
+    recording = factory.LazyAttribute(
+        lambda o: 'https://musicbrainz.org/recording/{}'.format(uuid.uuid4())
+    )
+    artist = factory.LazyAttribute(
+        lambda o: 'https://musicbrainz.org/artist/{}'.format(uuid.uuid4())
+    )
+    release = factory.LazyAttribute(
+        lambda o: 'https://musicbrainz.org/release/{}'.format(uuid.uuid4())
+    )
+
+    class Meta:
+        model = dict
+
+
+@registry.register(name='federation.Audio')
+class AudioFactory(factory.Factory):
+    type = 'Audio'
+    id = factory.Faker('url')
+    published = factory.LazyFunction(
+        lambda: timezone.now().isoformat()
+    )
+    actor = factory.Faker('url')
+    url = factory.SubFactory(LinkFactory, audio=True)
+    metadata = factory.SubFactory(LibraryTrackMetadataFactory)
+
+    class Meta:
+        model = dict
diff --git a/api/funkwhale_api/federation/filters.py b/api/funkwhale_api/federation/filters.py
new file mode 100644
index 0000000000000000000000000000000000000000..7a388ff1298fbab740342c5de1885157ea433a94
--- /dev/null
+++ b/api/funkwhale_api/federation/filters.py
@@ -0,0 +1,74 @@
+import django_filters
+
+from funkwhale_api.common import fields
+
+from . import models
+
+
+class LibraryFilter(django_filters.FilterSet):
+    approved = django_filters.BooleanFilter('following__approved')
+    q = fields.SearchFilter(search_fields=[
+        'actor__domain',
+    ])
+
+    class Meta:
+        model = models.Library
+        fields = {
+            'approved': ['exact'],
+            'federation_enabled': ['exact'],
+            'download_files': ['exact'],
+            'autoimport': ['exact'],
+            'tracks_count': ['exact'],
+        }
+
+
+class LibraryTrackFilter(django_filters.FilterSet):
+    library = django_filters.CharFilter('library__uuid')
+    imported = django_filters.CharFilter(method='filter_imported')
+    q = fields.SearchFilter(search_fields=[
+        'artist_name',
+        'title',
+        'album_title',
+        'library__actor__domain',
+    ])
+
+    def filter_imported(self, queryset, field_name, value):
+        if value.lower() in ['true', '1', 'yes']:
+            queryset = queryset.filter(local_track_file__isnull=False)
+        elif value.lower() in ['false', '0', 'no']:
+            queryset = queryset.filter(local_track_file__isnull=True)
+        return queryset
+
+    class Meta:
+        model = models.LibraryTrack
+        fields = {
+            'library': ['exact'],
+            'artist_name': ['exact', 'icontains'],
+            'title': ['exact', 'icontains'],
+            'album_title': ['exact', 'icontains'],
+            'audio_mimetype': ['exact', 'icontains'],
+        }
+
+
+class FollowFilter(django_filters.FilterSet):
+    pending = django_filters.CharFilter(method='filter_pending')
+    ordering = django_filters.OrderingFilter(
+        # tuple-mapping retains order
+        fields=(
+            ('creation_date', 'creation_date'),
+            ('modification_date', 'modification_date'),
+        ),
+    )
+    q = fields.SearchFilter(search_fields=[
+        'actor__domain',
+        'actor__preferred_username',
+    ])
+
+    class Meta:
+        model = models.Follow
+        fields = ['approved', 'pending', 'q']
+
+    def filter_pending(self, queryset, field_name, value):
+        if value.lower() in ['true', '1', 'yes']:
+            queryset = queryset.filter(approved__isnull=True)
+        return queryset
diff --git a/api/funkwhale_api/federation/keys.py b/api/funkwhale_api/federation/keys.py
index 08d4034ea347a6a17bb5d5701217d54cc1c57fa0..7e9d316c2582fa4535147265175bbea23e354ecf 100644
--- a/api/funkwhale_api/federation/keys.py
+++ b/api/funkwhale_api/federation/keys.py
@@ -3,7 +3,6 @@ from cryptography.hazmat.primitives.asymmetric import rsa
 from cryptography.hazmat.backends import default_backend as crypto_default_backend
 
 import re
-import requests
 import urllib.parse
 
 from . import exceptions
diff --git a/api/funkwhale_api/federation/library.py b/api/funkwhale_api/federation/library.py
new file mode 100644
index 0000000000000000000000000000000000000000..c53ce543067b7a014171ee44baeeafd99d7cc334
--- /dev/null
+++ b/api/funkwhale_api/federation/library.py
@@ -0,0 +1,166 @@
+import json
+import requests
+
+from django.conf import settings
+
+from funkwhale_api.common import session
+
+from . import actors
+from . import models
+from . import serializers
+from . import signing
+from . import webfinger
+
+
+def scan_from_account_name(account_name):
+    """
+    Given an account name such as library@test.library, will:
+
+    1. Perform the webfinger lookup
+    2. Perform the actor lookup
+    3. Perform the library's collection lookup
+
+    and return corresponding data in a dictionary.
+    """
+    data = {}
+    try:
+        username, domain = webfinger.clean_acct(
+            account_name, ensure_local=False)
+    except serializers.ValidationError:
+        return {
+            'webfinger': {
+                'errors': ['Invalid account string']
+            }
+        }
+    system_library = actors.SYSTEM_ACTORS['library'].get_actor_instance()
+    library = models.Library.objects.filter(
+        actor__domain=domain,
+        actor__preferred_username=username
+    ).select_related('actor').first()
+    data['local'] = {
+        'following': False,
+        'awaiting_approval': False,
+    }
+    try:
+        follow = models.Follow.objects.get(
+            target__preferred_username=username,
+            target__domain=username,
+            actor=system_library,
+        )
+        data['local']['awaiting_approval'] = not bool(follow.approved)
+        data['local']['following'] = True
+    except models.Follow.DoesNotExist:
+        pass
+
+    try:
+        data['webfinger'] = webfinger.get_resource(
+            'acct:{}'.format(account_name))
+    except requests.ConnectionError:
+        return {
+            'webfinger': {
+                'errors': ['This webfinger resource is not reachable']
+            }
+        }
+    except requests.HTTPError as e:
+        return {
+            'webfinger': {
+                'errors': [
+                    'Error {} during webfinger request'.format(
+                        e.response.status_code)]
+            }
+        }
+    except json.JSONDecodeError as e:
+        return {
+            'webfinger': {
+                'errors': ['Could not process webfinger response']
+            }
+        }
+
+    try:
+        data['actor'] = actors.get_actor_data(data['webfinger']['actor_url'])
+    except requests.ConnectionError:
+        data['actor'] = {
+            'errors': ['This actor is not reachable']
+        }
+        return data
+    except requests.HTTPError as e:
+        data['actor'] = {
+            'errors': [
+                'Error {} during actor request'.format(
+                    e.response.status_code)]
+        }
+        return data
+
+    serializer = serializers.LibraryActorSerializer(data=data['actor'])
+    if not serializer.is_valid():
+        data['actor'] = {
+            'errors': ['Invalid ActivityPub actor']
+        }
+        return data
+    data['library'] = get_library_data(
+        serializer.validated_data['library_url'])
+
+    return data
+
+
+def get_library_data(library_url):
+    actor = actors.SYSTEM_ACTORS['library'].get_actor_instance()
+    auth = signing.get_auth(actor.private_key, actor.private_key_id)
+    try:
+        response = session.get_session().get(
+            library_url,
+            auth=auth,
+            timeout=5,
+            verify=settings.EXTERNAL_REQUESTS_VERIFY_SSL,
+            headers={
+                'Content-Type': 'application/activity+json'
+            }
+        )
+    except requests.ConnectionError:
+        return {
+            'errors': ['This library is not reachable']
+        }
+    scode = response.status_code
+    if scode == 401:
+        return {
+            'errors': ['This library requires authentication']
+        }
+    elif scode == 403:
+        return {
+            'errors': ['Permission denied while scanning library']
+        }
+    elif scode >= 400:
+        return {
+            'errors': ['Error {} while fetching the library'.format(scode)]
+        }
+    serializer = serializers.PaginatedCollectionSerializer(
+        data=response.json(),
+    )
+    if not serializer.is_valid():
+        return {
+            'errors': [
+                'Invalid ActivityPub response from remote library']
+        }
+
+    return serializer.validated_data
+
+
+def get_library_page(library, page_url):
+    actor = actors.SYSTEM_ACTORS['library'].get_actor_instance()
+    auth = signing.get_auth(actor.private_key, actor.private_key_id)
+    response = session.get_session().get(
+        page_url,
+        auth=auth,
+        timeout=5,
+        verify=settings.EXTERNAL_REQUESTS_VERIFY_SSL,
+        headers={
+            'Content-Type': 'application/activity+json'
+        }
+    )
+    serializer = serializers.CollectionPageSerializer(
+        data=response.json(),
+        context={
+            'library': library,
+            'item_serializer': serializers.AudioSerializer})
+    serializer.is_valid(raise_exception=True)
+    return serializer.validated_data
diff --git a/api/funkwhale_api/federation/management/commands/__init__.py b/api/funkwhale_api/federation/management/commands/__init__.py
deleted file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0000000000000000000000000000000000000000
diff --git a/api/funkwhale_api/federation/management/commands/generate_keys.py b/api/funkwhale_api/federation/management/commands/generate_keys.py
deleted file mode 100644
index eafe9aae3477753a7b61cbc854152e3d95e26e59..0000000000000000000000000000000000000000
--- a/api/funkwhale_api/federation/management/commands/generate_keys.py
+++ /dev/null
@@ -1,53 +0,0 @@
-from django.core.management.base import BaseCommand, CommandError
-from django.db import transaction
-
-from dynamic_preferences.registries import global_preferences_registry
-
-from funkwhale_api.federation import keys
-
-
-class Command(BaseCommand):
-    help = (
-        'Generate a public/private key pair for your instance,'
-        ' for federation purposes. If a key pair already exists, does nothing.'
-    )
-
-    def add_arguments(self, parser):
-        parser.add_argument(
-            '--replace',
-            action='store_true',
-            dest='replace',
-            default=False,
-            help='Replace existing key pair, if any',
-        )
-        parser.add_argument(
-            '--noinput', '--no-input', action='store_false', dest='interactive',
-            help="Do NOT prompt the user for input of any kind.",
-        )
-
-    @transaction.atomic
-    def handle(self, *args, **options):
-        preferences = global_preferences_registry.manager()
-        existing_public = preferences['federation__public_key']
-        existing_private = preferences['federation__public_key']
-
-        if existing_public or existing_private and not options['replace']:
-            raise CommandError(
-                'Keys are already present! '
-                'Replace them with --replace if you know what you are doing.')
-
-        if options['interactive']:
-            message = (
-                'Are you sure you want to do this?\n\n'
-                "Type 'yes' to continue, or 'no' to cancel: "
-            )
-            if input(''.join(message)) != 'yes':
-                raise CommandError("Operation cancelled.")
-        private, public = keys.get_key_pair()
-        preferences['federation__public_key'] = public.decode('utf-8')
-        preferences['federation__private_key'] = private.decode('utf-8')
-
-        self.stdout.write(
-            'Your new key pair was generated.'
-            'Your public key is now:\n\n{}'.format(public.decode('utf-8'))
-        )
diff --git a/api/funkwhale_api/federation/migrations/0002_auto_20180403_1620.py b/api/funkwhale_api/federation/migrations/0002_auto_20180403_1620.py
new file mode 100644
index 0000000000000000000000000000000000000000..2200424d8e8cfe9a05c059e305d2533dfcda1121
--- /dev/null
+++ b/api/funkwhale_api/federation/migrations/0002_auto_20180403_1620.py
@@ -0,0 +1,17 @@
+# Generated by Django 2.0.3 on 2018-04-03 16:20
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('federation', '0001_initial'),
+    ]
+
+    operations = [
+        migrations.AlterUniqueTogether(
+            name='actor',
+            unique_together={('domain', 'preferred_username')},
+        ),
+    ]
diff --git a/api/funkwhale_api/federation/migrations/0003_auto_20180407_1010.py b/api/funkwhale_api/federation/migrations/0003_auto_20180407_1010.py
new file mode 100644
index 0000000000000000000000000000000000000000..12e3d73fed327dacfc57a1d4daf2b2099db8d093
--- /dev/null
+++ b/api/funkwhale_api/federation/migrations/0003_auto_20180407_1010.py
@@ -0,0 +1,94 @@
+# Generated by Django 2.0.3 on 2018-04-07 10:10
+
+import django.contrib.postgres.fields.jsonb
+from django.db import migrations, models
+import django.db.models.deletion
+import django.utils.timezone
+import uuid
+
+
+def delete_system_actors(apps, schema_editor):
+    """Revert site domain and name to default."""
+    Actor = apps.get_model("federation", "Actor")
+    Actor.objects.filter(preferred_username__in=['test', 'library']).delete()
+
+
+def backward(apps, schema_editor):
+    pass
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('federation', '0002_auto_20180403_1620'),
+    ]
+
+    operations = [
+        migrations.RunPython(delete_system_actors, backward),
+        migrations.CreateModel(
+            name='Follow',
+            fields=[
+                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+                ('uuid', models.UUIDField(default=uuid.uuid4, unique=True)),
+                ('creation_date', models.DateTimeField(default=django.utils.timezone.now)),
+                ('modification_date', models.DateTimeField(auto_now=True)),
+                ('actor', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='emitted_follows', to='federation.Actor')),
+                ('target', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='received_follows', to='federation.Actor')),
+            ],
+        ),
+        migrations.CreateModel(
+            name='FollowRequest',
+            fields=[
+                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+                ('uuid', models.UUIDField(default=uuid.uuid4, unique=True)),
+                ('creation_date', models.DateTimeField(default=django.utils.timezone.now)),
+                ('modification_date', models.DateTimeField(auto_now=True)),
+                ('approved', models.NullBooleanField(default=None)),
+                ('actor', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='emmited_follow_requests', to='federation.Actor')),
+                ('target', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='received_follow_requests', to='federation.Actor')),
+            ],
+        ),
+        migrations.CreateModel(
+            name='Library',
+            fields=[
+                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+                ('creation_date', models.DateTimeField(default=django.utils.timezone.now)),
+                ('modification_date', models.DateTimeField(auto_now=True)),
+                ('fetched_date', models.DateTimeField(blank=True, null=True)),
+                ('uuid', models.UUIDField(default=uuid.uuid4)),
+                ('url', models.URLField()),
+                ('federation_enabled', models.BooleanField()),
+                ('download_files', models.BooleanField()),
+                ('autoimport', models.BooleanField()),
+                ('tracks_count', models.PositiveIntegerField(blank=True, null=True)),
+                ('actor', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='library', to='federation.Actor')),
+            ],
+        ),
+        migrations.CreateModel(
+            name='LibraryTrack',
+            fields=[
+                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+                ('url', models.URLField(unique=True)),
+                ('audio_url', models.URLField()),
+                ('audio_mimetype', models.CharField(max_length=200)),
+                ('creation_date', models.DateTimeField(default=django.utils.timezone.now)),
+                ('modification_date', models.DateTimeField(auto_now=True)),
+                ('fetched_date', models.DateTimeField(blank=True, null=True)),
+                ('published_date', models.DateTimeField(blank=True, null=True)),
+                ('artist_name', models.CharField(max_length=500)),
+                ('album_title', models.CharField(max_length=500)),
+                ('title', models.CharField(max_length=500)),
+                ('metadata', django.contrib.postgres.fields.jsonb.JSONField(default={}, max_length=10000)),
+                ('library', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='tracks', to='federation.Library')),
+            ],
+        ),
+        migrations.AddField(
+            model_name='actor',
+            name='followers',
+            field=models.ManyToManyField(related_name='following', through='federation.Follow', to='federation.Actor'),
+        ),
+        migrations.AlterUniqueTogether(
+            name='follow',
+            unique_together={('actor', 'target')},
+        ),
+    ]
diff --git a/api/funkwhale_api/federation/migrations/0004_auto_20180410_2025.py b/api/funkwhale_api/federation/migrations/0004_auto_20180410_2025.py
new file mode 100644
index 0000000000000000000000000000000000000000..bea4d14ae6502272486b48bfa6f6f2074e06cb2c
--- /dev/null
+++ b/api/funkwhale_api/federation/migrations/0004_auto_20180410_2025.py
@@ -0,0 +1,35 @@
+# Generated by Django 2.0.3 on 2018-04-10 20:25
+
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('federation', '0003_auto_20180407_1010'),
+    ]
+
+    operations = [
+        migrations.RemoveField(
+            model_name='followrequest',
+            name='actor',
+        ),
+        migrations.RemoveField(
+            model_name='followrequest',
+            name='target',
+        ),
+        migrations.AddField(
+            model_name='follow',
+            name='approved',
+            field=models.NullBooleanField(default=None),
+        ),
+        migrations.AddField(
+            model_name='library',
+            name='follow',
+            field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='library', to='federation.Follow'),
+        ),
+        migrations.DeleteModel(
+            name='FollowRequest',
+        ),
+    ]
diff --git a/api/funkwhale_api/federation/migrations/0005_auto_20180413_1723.py b/api/funkwhale_api/federation/migrations/0005_auto_20180413_1723.py
new file mode 100644
index 0000000000000000000000000000000000000000..00ba5c83deb9316ab8d509ba5e859e2fe979aafd
--- /dev/null
+++ b/api/funkwhale_api/federation/migrations/0005_auto_20180413_1723.py
@@ -0,0 +1,26 @@
+# Generated by Django 2.0.3 on 2018-04-13 17:23
+
+import django.contrib.postgres.fields.jsonb
+import django.core.serializers.json
+from django.db import migrations, models
+import funkwhale_api.federation.models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('federation', '0004_auto_20180410_2025'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='librarytrack',
+            name='audio_file',
+            field=models.FileField(blank=True, null=True, upload_to=funkwhale_api.federation.models.get_file_path),
+        ),
+        migrations.AlterField(
+            model_name='librarytrack',
+            name='metadata',
+            field=django.contrib.postgres.fields.jsonb.JSONField(default={}, encoder=django.core.serializers.json.DjangoJSONEncoder, max_length=10000),
+        ),
+    ]
diff --git a/api/funkwhale_api/federation/models.py b/api/funkwhale_api/federation/models.py
index d76ad173be80c0d37dc063a7b0d829952f9e16db..d91a00c8b50f5c103fc818f9dea47f6a55cbf9cf 100644
--- a/api/funkwhale_api/federation/models.py
+++ b/api/funkwhale_api/federation/models.py
@@ -1,7 +1,16 @@
+import os
+import uuid
+import tempfile
+
 from django.conf import settings
+from django.contrib.postgres.fields import JSONField
+from django.core.serializers.json import DjangoJSONEncoder
 from django.db import models
 from django.utils import timezone
 
+from funkwhale_api.common import session
+from funkwhale_api.music import utils as music_utils
+
 TYPE_CHOICES = [
     ('Person', 'Person'),
     ('Application', 'Application'),
@@ -12,6 +21,8 @@ TYPE_CHOICES = [
 
 
 class Actor(models.Model):
+    ap_type = 'Actor'
+
     url = models.URLField(unique=True, max_length=500, db_index=True)
     outbox_url = models.URLField(max_length=500)
     inbox_url = models.URLField(max_length=500)
@@ -31,6 +42,16 @@ class Actor(models.Model):
     last_fetch_date = models.DateTimeField(
         default=timezone.now)
     manually_approves_followers = models.NullBooleanField(default=None)
+    followers = models.ManyToManyField(
+        to='self',
+        symmetrical=False,
+        through='Follow',
+        through_fields=('target', 'actor'),
+        related_name='following',
+    )
+
+    class Meta:
+        unique_together = ['domain', 'preferred_username']
 
     @property
     def webfinger_subject(self):
@@ -57,3 +78,141 @@ class Actor(models.Model):
                 setattr(self, field, v.lower())
 
         super().save(**kwargs)
+
+    @property
+    def is_local(self):
+        return self.domain == settings.FEDERATION_HOSTNAME
+
+    @property
+    def is_system(self):
+        from . import actors
+        return all([
+            settings.FEDERATION_HOSTNAME == self.domain,
+            self.preferred_username in actors.SYSTEM_ACTORS
+        ])
+
+    @property
+    def system_conf(self):
+        from . import actors
+        if self.is_system:
+            return actors.SYSTEM_ACTORS[self.preferred_username]
+
+    def get_approved_followers(self):
+        follows = self.received_follows.filter(approved=True)
+        return self.followers.filter(
+            pk__in=follows.values_list('actor', flat=True))
+
+
+class Follow(models.Model):
+    ap_type = 'Follow'
+
+    uuid = models.UUIDField(default=uuid.uuid4, unique=True)
+    actor = models.ForeignKey(
+        Actor,
+        related_name='emitted_follows',
+        on_delete=models.CASCADE,
+    )
+    target = models.ForeignKey(
+        Actor,
+        related_name='received_follows',
+        on_delete=models.CASCADE,
+    )
+    creation_date = models.DateTimeField(default=timezone.now)
+    modification_date = models.DateTimeField(
+        auto_now=True)
+    approved = models.NullBooleanField(default=None)
+
+    class Meta:
+        unique_together = ['actor', 'target']
+
+    def get_federation_url(self):
+        return '{}#follows/{}'.format(self.actor.url, self.uuid)
+
+
+class Library(models.Model):
+    creation_date = models.DateTimeField(default=timezone.now)
+    modification_date = models.DateTimeField(
+        auto_now=True)
+    fetched_date = models.DateTimeField(null=True, blank=True)
+    actor = models.OneToOneField(
+        Actor,
+        on_delete=models.CASCADE,
+        related_name='library')
+    uuid = models.UUIDField(default=uuid.uuid4)
+    url = models.URLField()
+
+    # use this flag to disable federation with a library
+    federation_enabled = models.BooleanField()
+    # should we mirror files locally or hotlink them?
+    download_files = models.BooleanField()
+    # should we automatically import new files from this library?
+    autoimport = models.BooleanField()
+    tracks_count = models.PositiveIntegerField(null=True, blank=True)
+    follow = models.OneToOneField(
+        Follow,
+        related_name='library',
+        null=True,
+        blank=True,
+        on_delete=models.SET_NULL,
+    )
+
+
+def get_file_path(instance, filename):
+    uid = str(uuid.uuid4())
+    chunk_size = 2
+    chunks = [uid[i:i+chunk_size] for i in range(0, len(uid), chunk_size)]
+    parts = chunks[:3] + [filename]
+    return os.path.join('federation_cache', *parts)
+
+
+class LibraryTrack(models.Model):
+    url = models.URLField(unique=True)
+    audio_url = models.URLField()
+    audio_mimetype = models.CharField(max_length=200)
+    audio_file = models.FileField(
+        upload_to=get_file_path,
+        null=True,
+        blank=True)
+
+    creation_date = models.DateTimeField(default=timezone.now)
+    modification_date = models.DateTimeField(
+        auto_now=True)
+    fetched_date = models.DateTimeField(null=True, blank=True)
+    published_date = models.DateTimeField(null=True, blank=True)
+    library = models.ForeignKey(
+        Library, related_name='tracks', on_delete=models.CASCADE)
+    artist_name = models.CharField(max_length=500)
+    album_title = models.CharField(max_length=500)
+    title = models.CharField(max_length=500)
+    metadata = JSONField(
+        default={}, max_length=10000, encoder=DjangoJSONEncoder)
+
+    @property
+    def mbid(self):
+        try:
+            return self.metadata['recording']['musicbrainz_id']
+        except KeyError:
+            pass
+
+    def download_audio(self):
+        from . import actors
+        auth = actors.SYSTEM_ACTORS['library'].get_request_auth()
+        remote_response = session.get_session().get(
+            self.audio_url,
+            auth=auth,
+            stream=True,
+            timeout=20,
+            verify=settings.EXTERNAL_REQUESTS_VERIFY_SSL,
+            headers={
+                'Content-Type': 'application/activity+json'
+            }
+        )
+        with remote_response as r:
+            remote_response.raise_for_status()
+            extension = music_utils.get_ext_from_type(self.audio_mimetype)
+            title = ' - '.join([self.title, self.album_title, self.artist_name])
+            filename = '{}.{}'.format(title, extension)
+            tmp_file = tempfile.TemporaryFile()
+            for chunk in r.iter_content(chunk_size=512):
+                tmp_file.write(chunk)
+            self.audio_file.save(filename, tmp_file)
diff --git a/api/funkwhale_api/federation/permissions.py b/api/funkwhale_api/federation/permissions.py
new file mode 100644
index 0000000000000000000000000000000000000000..370328eaa8c79bf60a9e5ff6632896565601ced6
--- /dev/null
+++ b/api/funkwhale_api/federation/permissions.py
@@ -0,0 +1,19 @@
+from django.conf import settings
+
+from rest_framework.permissions import BasePermission
+
+from . import actors
+
+
+class LibraryFollower(BasePermission):
+
+    def has_permission(self, request, view):
+        if not settings.FEDERATION_MUSIC_NEEDS_APPROVAL:
+            return True
+
+        actor = getattr(request, 'actor', None)
+        if actor is None:
+            return False
+
+        library = actors.SYSTEM_ACTORS['library'].get_actor_instance()
+        return library.followers.filter(url=actor.url).exists()
diff --git a/api/funkwhale_api/federation/serializers.py b/api/funkwhale_api/federation/serializers.py
index 2137e8d910373e0c8d23b3e38c0d9952e4a93787..b56dd3f44b6bd7ca6e07b9e2740fada8f5d2e65b 100644
--- a/api/funkwhale_api/federation/serializers.py
+++ b/api/funkwhale_api/federation/serializers.py
@@ -2,52 +2,59 @@ import urllib.parse
 
 from django.urls import reverse
 from django.conf import settings
+from django.core.paginator import Paginator
+from django.db import transaction
 
 from rest_framework import serializers
 from dynamic_preferences.registries import global_preferences_registry
 
+from funkwhale_api.common import utils as funkwhale_utils
+
 from . import activity
 from . import models
 from . import utils
 
 
-class ActorSerializer(serializers.ModelSerializer):
-    # left maps to activitypub fields, right to our internal models
-    id = serializers.URLField(source='url')
-    outbox = serializers.URLField(source='outbox_url')
-    inbox = serializers.URLField(source='inbox_url')
-    following = serializers.URLField(source='following_url', required=False)
-    followers = serializers.URLField(source='followers_url', required=False)
-    preferredUsername = serializers.CharField(
-        source='preferred_username', required=False)
-    publicKey = serializers.JSONField(source='public_key', required=False)
-    manuallyApprovesFollowers = serializers.NullBooleanField(
-        source='manually_approves_followers', required=False)
-    summary = serializers.CharField(max_length=None, required=False)
+AP_CONTEXT = [
+    'https://www.w3.org/ns/activitystreams',
+    'https://w3id.org/security/v1',
+    {},
+]
 
-    class Meta:
-        model = models.Actor
-        fields = [
-            'id',
-            'type',
-            'name',
-            'summary',
-            'preferredUsername',
-            'publicKey',
-            'inbox',
-            'outbox',
-            'following',
-            'followers',
-            'manuallyApprovesFollowers',
-        ]
+
+class ActorSerializer(serializers.Serializer):
+    id = serializers.URLField()
+    outbox = serializers.URLField()
+    inbox = serializers.URLField()
+    type = serializers.ChoiceField(choices=models.TYPE_CHOICES)
+    preferredUsername = serializers.CharField()
+    manuallyApprovesFollowers = serializers.NullBooleanField(required=False)
+    name = serializers.CharField(required=False, max_length=200)
+    summary = serializers.CharField(max_length=None, required=False)
+    followers = serializers.URLField(required=False, allow_null=True)
+    following = serializers.URLField(required=False, allow_null=True)
+    publicKey = serializers.JSONField(required=False)
 
     def to_representation(self, instance):
-        ret = super().to_representation(instance)
-        ret['@context'] = [
-            'https://www.w3.org/ns/activitystreams',
-            'https://w3id.org/security/v1',
-            {},
-        ]
+        ret = {
+            'id': instance.url,
+            'outbox': instance.outbox_url,
+            'inbox': instance.inbox_url,
+            'preferredUsername': instance.preferred_username,
+            'type': instance.type,
+        }
+        if instance.name:
+            ret['name'] = instance.name
+        if instance.followers_url:
+            ret['followers'] = instance.followers_url
+        if instance.following_url:
+            ret['following'] = instance.following_url
+        if instance.summary:
+            ret['summary'] = instance.summary
+        if instance.manually_approves_followers is not None:
+            ret['manuallyApprovesFollowers'] = instance.manually_approves_followers
+
+        ret['@context'] = AP_CONTEXT
         if instance.public_key:
             ret['publicKey'] = {
                 'owner': instance.url,
@@ -60,8 +67,21 @@ class ActorSerializer(serializers.ModelSerializer):
         return ret
 
     def prepare_missing_fields(self):
-        kwargs = {}
-        domain = urllib.parse.urlparse(self.validated_data['url']).netloc
+        kwargs = {
+            'url': self.validated_data['id'],
+            'outbox_url': self.validated_data['outbox'],
+            'inbox_url': self.validated_data['inbox'],
+            'following_url': self.validated_data.get('following'),
+            'followers_url': self.validated_data.get('followers'),
+            'summary': self.validated_data.get('summary'),
+            'type': self.validated_data['type'],
+            'name': self.validated_data.get('name'),
+            'preferred_username': self.validated_data['preferredUsername'],
+        }
+        maf = self.validated_data.get('manuallyApprovesFollowers')
+        if maf is not None:
+            kwargs['manually_approves_followers'] = maf
+        domain = urllib.parse.urlparse(kwargs['url']).netloc
         kwargs['domain'] = domain
         for endpoint, url in self.initial_data.get('endpoints', {}).items():
             if endpoint == 'sharedInbox':
@@ -74,23 +94,386 @@ class ActorSerializer(serializers.ModelSerializer):
         return kwargs
 
     def build(self):
-        d = self.validated_data.copy()
-        d.update(self.prepare_missing_fields())
-        return self.Meta.model(**d)
+        d = self.prepare_missing_fields()
+        return models.Actor(**d)
 
     def save(self, **kwargs):
-        kwargs.update(self.prepare_missing_fields())
-        return super().save(**kwargs)
+        d = self.prepare_missing_fields()
+        d.update(kwargs)
+        return models.Actor.objects.create(
+            **d
+        )
 
     def validate_summary(self, value):
         if value:
             return value[:500]
 
 
-class ActorWebfingerSerializer(serializers.ModelSerializer):
+class APIActorSerializer(serializers.ModelSerializer):
     class Meta:
         model = models.Actor
-        fields = ['url']
+        fields = [
+            'id',
+            'url',
+            'creation_date',
+            'summary',
+            'preferred_username',
+            'name',
+            'last_fetch_date',
+            'domain',
+            'type',
+            'manually_approves_followers',
+
+        ]
+
+
+class LibraryActorSerializer(ActorSerializer):
+    url = serializers.ListField(
+        child=serializers.JSONField())
+
+    def validate(self, validated_data):
+        try:
+            urls = validated_data['url']
+        except KeyError:
+            raise serializers.ValidationError('Missing URL field')
+
+        for u in urls:
+            try:
+                if u['name'] != 'library':
+                    continue
+                validated_data['library_url'] = u['href']
+                break
+            except KeyError:
+                continue
+
+        return validated_data
+
+
+class APIFollowSerializer(serializers.ModelSerializer):
+    class Meta:
+        model = models.Follow
+        fields = [
+            'uuid',
+            'actor',
+            'target',
+            'approved',
+            'creation_date',
+            'modification_date',
+        ]
+
+
+class APILibrarySerializer(serializers.ModelSerializer):
+    actor = APIActorSerializer()
+    follow = APIFollowSerializer()
+
+    class Meta:
+        model = models.Library
+
+        read_only_fields = [
+            'actor',
+            'uuid',
+            'url',
+            'tracks_count',
+            'follow',
+            'fetched_date',
+            'modification_date',
+            'creation_date',
+        ]
+        fields = [
+            'autoimport',
+            'federation_enabled',
+            'download_files',
+        ] + read_only_fields
+
+
+class APILibraryScanSerializer(serializers.Serializer):
+    until = serializers.DateTimeField(required=False)
+
+
+class APILibraryFollowUpdateSerializer(serializers.Serializer):
+    follow = serializers.IntegerField()
+    approved = serializers.BooleanField()
+
+    def validate_follow(self, value):
+        from . import actors
+        library_actor = actors.SYSTEM_ACTORS['library'].get_actor_instance()
+        qs = models.Follow.objects.filter(
+            pk=value,
+            target=library_actor,
+        )
+        try:
+            return qs.get()
+        except models.Follow.DoesNotExist:
+            raise serializers.ValidationError('Invalid follow')
+
+    def save(self):
+        new_status = self.validated_data['approved']
+        follow = self.validated_data['follow']
+        if new_status == follow.approved:
+            return follow
+
+        follow.approved = new_status
+        follow.save(update_fields=['approved', 'modification_date'])
+        if new_status:
+            activity.accept_follow(follow)
+        return follow
+
+
+class APILibraryCreateSerializer(serializers.ModelSerializer):
+    actor = serializers.URLField()
+    federation_enabled = serializers.BooleanField()
+    uuid = serializers.UUIDField(read_only=True)
+
+    class Meta:
+        model = models.Library
+        fields = [
+            'uuid',
+            'actor',
+            'autoimport',
+            'federation_enabled',
+            'download_files',
+        ]
+
+    def validate(self, validated_data):
+        from . import actors
+        from . import library
+
+        actor_url = validated_data['actor']
+        actor_data = actors.get_actor_data(actor_url)
+        acs = LibraryActorSerializer(data=actor_data)
+        acs.is_valid(raise_exception=True)
+        try:
+            actor = models.Actor.objects.get(url=actor_url)
+        except models.Actor.DoesNotExist:
+            actor = acs.save()
+        library_actor = actors.SYSTEM_ACTORS['library'].get_actor_instance()
+        validated_data['follow'] = models.Follow.objects.get_or_create(
+            actor=library_actor,
+            target=actor,
+        )[0]
+        if validated_data['follow'].approved is None:
+            funkwhale_utils.on_commit(
+                activity.deliver,
+                FollowSerializer(validated_data['follow']).data,
+                on_behalf_of=validated_data['follow'].actor,
+                to=[validated_data['follow'].target.url],
+            )
+
+        library_data = library.get_library_data(
+            acs.validated_data['library_url'])
+        if 'errors' in library_data:
+            # we pass silently because it may means we require permission
+            # before scanning
+            pass
+        validated_data['library'] = library_data
+        validated_data['library'].setdefault(
+            'id', acs.validated_data['library_url']
+        )
+        validated_data['actor'] = actor
+        return validated_data
+
+    def create(self, validated_data):
+        library = models.Library.objects.update_or_create(
+            url=validated_data['library']['id'],
+            defaults={
+                'actor': validated_data['actor'],
+                'follow': validated_data['follow'],
+                'tracks_count': validated_data['library'].get('totalItems'),
+                'federation_enabled': validated_data['federation_enabled'],
+                'autoimport': validated_data['autoimport'],
+                'download_files': validated_data['download_files'],
+            }
+        )[0]
+        return library
+
+
+class APILibraryTrackSerializer(serializers.ModelSerializer):
+    library = APILibrarySerializer()
+
+    class Meta:
+        model = models.LibraryTrack
+        fields = [
+            'id',
+            'url',
+            'audio_url',
+            'audio_mimetype',
+            'creation_date',
+            'modification_date',
+            'fetched_date',
+            'published_date',
+            'metadata',
+            'artist_name',
+            'album_title',
+            'title',
+            'library',
+            'local_track_file',
+        ]
+
+
+class FollowSerializer(serializers.Serializer):
+    id = serializers.URLField()
+    object = serializers.URLField()
+    actor = serializers.URLField()
+    type = serializers.ChoiceField(choices=['Follow'])
+
+    def validate_object(self, v):
+        expected = self.context.get('follow_target')
+        if expected and expected.url != v:
+            raise serializers.ValidationError('Invalid target')
+        try:
+            return models.Actor.objects.get(url=v)
+        except models.Actor.DoesNotExist:
+            raise serializers.ValidationError('Target not found')
+
+    def validate_actor(self, v):
+        expected = self.context.get('follow_actor')
+        if expected and expected.url != v:
+            raise serializers.ValidationError('Invalid actor')
+        try:
+            return models.Actor.objects.get(url=v)
+        except models.Actor.DoesNotExist:
+            raise serializers.ValidationError('Actor not found')
+
+    def save(self, **kwargs):
+        return models.Follow.objects.get_or_create(
+            actor=self.validated_data['actor'],
+            target=self.validated_data['object'],
+            **kwargs,
+        )[0]
+
+    def to_representation(self, instance):
+        return {
+            '@context': AP_CONTEXT,
+            'actor': instance.actor.url,
+            'id': instance.get_federation_url(),
+            'object': instance.target.url,
+            'type': 'Follow'
+        }
+        return ret
+
+
+class APIFollowSerializer(serializers.ModelSerializer):
+    actor = APIActorSerializer()
+    target = APIActorSerializer()
+
+    class Meta:
+        model = models.Follow
+        fields = [
+            'uuid',
+            'id',
+            'approved',
+            'creation_date',
+            'modification_date',
+            'actor',
+            'target',
+        ]
+
+
+class AcceptFollowSerializer(serializers.Serializer):
+    id = serializers.URLField()
+    actor = serializers.URLField()
+    object = FollowSerializer()
+    type = serializers.ChoiceField(choices=['Accept'])
+
+    def validate_actor(self, v):
+        expected = self.context.get('follow_target')
+        if expected and expected.url != v:
+            raise serializers.ValidationError('Invalid actor')
+        try:
+            return models.Actor.objects.get(url=v)
+        except models.Actor.DoesNotExist:
+            raise serializers.ValidationError('Actor not found')
+
+    def validate(self, validated_data):
+        # we ensure the accept actor actually match the follow target
+        if validated_data['actor'] != validated_data['object']['object']:
+            raise serializers.ValidationError('Actor mismatch')
+        try:
+            validated_data['follow'] = models.Follow.objects.filter(
+                target=validated_data['actor'],
+                actor=validated_data['object']['actor']
+            ).exclude(approved=True).get()
+        except models.Follow.DoesNotExist:
+            raise serializers.ValidationError('No follow to accept')
+        return validated_data
+
+    def to_representation(self, instance):
+        return {
+            "@context": AP_CONTEXT,
+            "id": instance.get_federation_url() + '/accept',
+            "type": "Accept",
+            "actor": instance.target.url,
+            "object": FollowSerializer(instance).data
+        }
+
+    def save(self):
+        self.validated_data['follow'].approved = True
+        self.validated_data['follow'].save()
+        return self.validated_data['follow']
+
+
+class UndoFollowSerializer(serializers.Serializer):
+    id = serializers.URLField()
+    actor = serializers.URLField()
+    object = FollowSerializer()
+    type = serializers.ChoiceField(choices=['Undo'])
+
+    def validate_actor(self, v):
+        expected = self.context.get('follow_target')
+        if expected and expected.url != v:
+            raise serializers.ValidationError('Invalid actor')
+        try:
+            return models.Actor.objects.get(url=v)
+        except models.Actor.DoesNotExist:
+            raise serializers.ValidationError('Actor not found')
+
+    def validate(self, validated_data):
+        # we ensure the accept actor actually match the follow actor
+        if validated_data['actor'] != validated_data['object']['actor']:
+            raise serializers.ValidationError('Actor mismatch')
+        try:
+            validated_data['follow'] = models.Follow.objects.filter(
+                actor=validated_data['actor'],
+                target=validated_data['object']['object']
+            ).get()
+        except models.Follow.DoesNotExist:
+            raise serializers.ValidationError('No follow to remove')
+        return validated_data
+
+    def to_representation(self, instance):
+        return {
+            "@context": AP_CONTEXT,
+            "id": instance.get_federation_url() + '/undo',
+            "type": "Undo",
+            "actor": instance.actor.url,
+            "object": FollowSerializer(instance).data
+        }
+
+    def save(self):
+        return self.validated_data['follow'].delete()
+
+
+class ActorWebfingerSerializer(serializers.Serializer):
+    subject = serializers.CharField()
+    aliases = serializers.ListField(child=serializers.URLField())
+    links = serializers.ListField()
+    actor_url = serializers.URLField(required=False)
+
+    def validate(self, validated_data):
+        validated_data['actor_url'] = None
+        for l in validated_data['links']:
+            try:
+                if not l['rel'] == 'self':
+                    continue
+                if not l['type'] == 'application/activity+json':
+                    continue
+                validated_data['actor_url'] = l['href']
+                break
+            except KeyError:
+                pass
+        if validated_data['actor_url'] is None:
+            raise serializers.ValidationError('No valid actor url found')
+        return validated_data
 
     def to_representation(self, instance):
         data = {}
@@ -110,7 +493,7 @@ class ActorWebfingerSerializer(serializers.ModelSerializer):
 
 class ActivitySerializer(serializers.Serializer):
     actor = serializers.URLField()
-    id = serializers.URLField()
+    id = serializers.URLField(required=False)
     type = serializers.ChoiceField(
         choices=[(c, c) for c in activity.ACTIVITY_TYPES])
     object = serializers.JSONField()
@@ -120,7 +503,9 @@ class ActivitySerializer(serializers.Serializer):
             type = value['type']
         except KeyError:
             raise serializers.ValidationError('Missing object type')
-
+        except TypeError:
+            # probably a URL
+            return value
         try:
             object_serializer = OBJECT_SERIALIZERS[type]
         except KeyError:
@@ -140,6 +525,14 @@ class ActivitySerializer(serializers.Serializer):
             )
         return value
 
+    def to_representation(self, conf):
+        d = {}
+        d.update(conf)
+
+        if self.context.get('include_ap_context', True):
+            d['@context'] = AP_CONTEXT
+        return d
+
 
 class ObjectSerializer(serializers.Serializer):
     id = serializers.URLField()
@@ -173,3 +566,242 @@ OBJECT_SERIALIZERS = {
     t: ObjectSerializer
     for t in activity.OBJECT_TYPES
 }
+
+
+class PaginatedCollectionSerializer(serializers.Serializer):
+    type = serializers.ChoiceField(choices=['Collection'])
+    totalItems = serializers.IntegerField(min_value=0)
+    actor = serializers.URLField()
+    id = serializers.URLField()
+    first = serializers.URLField()
+    last = serializers.URLField()
+
+    def to_representation(self, conf):
+        paginator = Paginator(
+            conf['items'],
+            conf.get('page_size', 20)
+        )
+        first = funkwhale_utils.set_query_parameter(conf['id'], page=1)
+        current = first
+        last = funkwhale_utils.set_query_parameter(
+            conf['id'], page=paginator.num_pages)
+        d = {
+            'id': conf['id'],
+            'actor': conf['actor'].url,
+            'totalItems': paginator.count,
+            'type': 'Collection',
+            'current': current,
+            'first': first,
+            'last': last,
+        }
+        if self.context.get('include_ap_context', True):
+            d['@context'] = AP_CONTEXT
+        return d
+
+
+class CollectionPageSerializer(serializers.Serializer):
+    type = serializers.ChoiceField(choices=['CollectionPage'])
+    totalItems = serializers.IntegerField(min_value=0)
+    items = serializers.ListField()
+    actor = serializers.URLField()
+    id = serializers.URLField()
+    first = serializers.URLField()
+    last = serializers.URLField()
+    next = serializers.URLField(required=False)
+    prev = serializers.URLField(required=False)
+    partOf = serializers.URLField()
+
+    def validate_items(self, v):
+        item_serializer = self.context.get('item_serializer')
+        if not item_serializer:
+            return v
+        raw_items = [item_serializer(data=i, context=self.context) for i in v]
+        for i in raw_items:
+            i.is_valid(raise_exception=True)
+
+        return raw_items
+
+    def to_representation(self, conf):
+        page = conf['page']
+        first = funkwhale_utils.set_query_parameter(
+            conf['id'], page=1)
+        last = funkwhale_utils.set_query_parameter(
+            conf['id'], page=page.paginator.num_pages)
+        id = funkwhale_utils.set_query_parameter(
+            conf['id'], page=page.number)
+        d = {
+            'id': id,
+            'partOf': conf['id'],
+            'actor': conf['actor'].url,
+            'totalItems': page.paginator.count,
+            'type': 'CollectionPage',
+            'first': first,
+            'last': last,
+            'items': [
+                conf['item_serializer'](
+                    i,
+                    context={
+                        'actor': conf['actor'],
+                        'include_ap_context': False}
+                ).data
+                for i in page.object_list
+            ]
+        }
+
+        if page.has_previous():
+            d['prev'] = funkwhale_utils.set_query_parameter(
+                conf['id'], page=page.previous_page_number())
+
+        if page.has_next():
+            d['next'] = funkwhale_utils.set_query_parameter(
+                conf['id'], page=page.next_page_number())
+
+        if self.context.get('include_ap_context', True):
+            d['@context'] = AP_CONTEXT
+        return d
+
+
+class ArtistMetadataSerializer(serializers.Serializer):
+    musicbrainz_id = serializers.UUIDField(required=False)
+    name = serializers.CharField()
+
+
+class ReleaseMetadataSerializer(serializers.Serializer):
+    musicbrainz_id = serializers.UUIDField(required=False)
+    title = serializers.CharField()
+
+
+class RecordingMetadataSerializer(serializers.Serializer):
+    musicbrainz_id = serializers.UUIDField(required=False)
+    title = serializers.CharField()
+
+
+class AudioMetadataSerializer(serializers.Serializer):
+    artist = ArtistMetadataSerializer()
+    release = ReleaseMetadataSerializer()
+    recording = RecordingMetadataSerializer()
+
+
+class AudioSerializer(serializers.Serializer):
+    type = serializers.CharField()
+    id = serializers.URLField()
+    url = serializers.JSONField()
+    published = serializers.DateTimeField()
+    updated = serializers.DateTimeField(required=False)
+    metadata = AudioMetadataSerializer()
+
+    def validate_type(self, v):
+        if v != 'Audio':
+            raise serializers.ValidationError('Invalid type for audio')
+        return v
+
+    def validate_url(self, v):
+        try:
+            url = v['href']
+        except (KeyError, TypeError):
+            raise serializers.ValidationError('Missing href')
+
+        try:
+            media_type = v['mediaType']
+        except (KeyError, TypeError):
+            raise serializers.ValidationError('Missing mediaType')
+
+        if not media_type.startswith('audio/'):
+            raise serializers.ValidationError('Invalid mediaType')
+
+        return url
+
+    def validate_url(self, v):
+        try:
+            url = v['href']
+        except (KeyError, TypeError):
+            raise serializers.ValidationError('Missing href')
+
+        try:
+            media_type = v['mediaType']
+        except (KeyError, TypeError):
+            raise serializers.ValidationError('Missing mediaType')
+
+        if not media_type.startswith('audio/'):
+            raise serializers.ValidationError('Invalid mediaType')
+
+        return v
+
+    def create(self, validated_data):
+        defaults = {
+            'audio_mimetype': validated_data['url']['mediaType'],
+            'audio_url': validated_data['url']['href'],
+            'metadata': validated_data['metadata'],
+            'artist_name': validated_data['metadata']['artist']['name'],
+            'album_title': validated_data['metadata']['release']['title'],
+            'title': validated_data['metadata']['recording']['title'],
+            'published_date': validated_data['published'],
+            'modification_date': validated_data.get('updated'),
+        }
+        return models.LibraryTrack.objects.get_or_create(
+            library=self.context['library'],
+            url=validated_data['id'],
+            defaults=defaults
+        )[0]
+
+    def to_representation(self, instance):
+        track = instance.track
+        album = instance.track.album
+        artist = instance.track.artist
+
+        d = {
+            'type': 'Audio',
+            'id': instance.get_federation_url(),
+            'name': instance.track.full_name,
+            'published': instance.creation_date.isoformat(),
+            'updated': instance.modification_date.isoformat(),
+            'metadata': {
+                'artist': {
+                    'musicbrainz_id': str(artist.mbid) if artist.mbid else None,
+                    'name': artist.name,
+                },
+                'release': {
+                    'musicbrainz_id': str(album.mbid) if album.mbid else None,
+                    'title': album.title,
+                },
+                'recording': {
+                    'musicbrainz_id': str(track.mbid) if track.mbid else None,
+                    'title': track.title,
+                },
+            },
+            'url': {
+                'href': utils.full_url(instance.path),
+                'type': 'Link',
+                'mediaType': instance.mimetype
+            },
+            'attributedTo': [
+                self.context['actor'].url
+            ]
+        }
+        if self.context.get('include_ap_context', True):
+            d['@context'] = AP_CONTEXT
+        return d
+
+
+class CollectionSerializer(serializers.Serializer):
+
+    def to_representation(self, conf):
+        d = {
+            'id': conf['id'],
+            'actor': conf['actor'].url,
+            'totalItems': len(conf['items']),
+            'type': 'Collection',
+            'items': [
+                conf['item_serializer'](
+                    i,
+                    context={
+                        'actor': conf['actor'],
+                        'include_ap_context': False}
+                ).data
+                for i in conf['items']
+            ]
+        }
+
+        if self.context.get('include_ap_context', True):
+            d['@context'] = AP_CONTEXT
+        return d
diff --git a/api/funkwhale_api/federation/signing.py b/api/funkwhale_api/federation/signing.py
index 7e4d2aa5ae08748ea5b6975aa7345d33f993ab1c..8d984d3ffd01cdbdcf8e66dabdeb4065bd3435bf 100644
--- a/api/funkwhale_api/federation/signing.py
+++ b/api/funkwhale_api/federation/signing.py
@@ -53,3 +53,18 @@ def verify_django(django_request, public_key):
             request.headers[h] = str(v)
     prepared_request = request.prepare()
     return verify(request, public_key)
+
+
+def get_auth(private_key, private_key_id):
+    return requests_http_signature.HTTPSignatureAuth(
+        use_auth_header=False,
+        headers=[
+            '(request-target)',
+            'user-agent',
+            'host',
+            'date',
+            'content-type'],
+        algorithm='rsa-sha256',
+        key=private_key.encode('utf-8'),
+        key_id=private_key_id,
+    )
diff --git a/api/funkwhale_api/federation/tasks.py b/api/funkwhale_api/federation/tasks.py
new file mode 100644
index 0000000000000000000000000000000000000000..adc354c4fdaba7f1195e960246e6ff2f6161b107
--- /dev/null
+++ b/api/funkwhale_api/federation/tasks.py
@@ -0,0 +1,111 @@
+import datetime
+import json
+import logging
+
+from django.conf import settings
+from django.utils import timezone
+
+from requests.exceptions import RequestException
+from dynamic_preferences.registries import global_preferences_registry
+
+from funkwhale_api.common import session
+from funkwhale_api.history.models import Listening
+from funkwhale_api.taskapp import celery
+
+from . import actors
+from . import library as lb
+from . import models
+from . import signing
+
+
+logger = logging.getLogger(__name__)
+
+
+@celery.app.task(
+    name='federation.send',
+    autoretry_for=[RequestException],
+    retry_backoff=30,
+    max_retries=5)
+@celery.require_instance(models.Actor, 'actor')
+def send(activity, actor, to):
+    logger.info('Preparing activity delivery to %s', to)
+    auth = signing.get_auth(
+        actor.private_key, actor.private_key_id)
+    for url in to:
+        recipient_actor = actors.get_actor(url)
+        logger.debug('delivering to %s', recipient_actor.inbox_url)
+        logger.debug('activity content: %s', json.dumps(activity))
+        response = session.get_session().post(
+            auth=auth,
+            json=activity,
+            url=recipient_actor.inbox_url,
+            timeout=5,
+            verify=settings.EXTERNAL_REQUESTS_VERIFY_SSL,
+            headers={
+                'Content-Type': 'application/activity+json'
+            }
+        )
+        response.raise_for_status()
+        logger.debug('Remote answered with %s', response.status_code)
+
+
+@celery.app.task(
+    name='federation.scan_library',
+    autoretry_for=[RequestException],
+    retry_backoff=30,
+    max_retries=5)
+@celery.require_instance(models.Library, 'library')
+def scan_library(library, until=None):
+    if not library.federation_enabled:
+        return
+
+    data = lb.get_library_data(library.url)
+    scan_library_page.delay(
+        library_id=library.id, page_url=data['first'], until=until)
+    library.fetched_date = timezone.now()
+    library.tracks_count = data['totalItems']
+    library.save(update_fields=['fetched_date', 'tracks_count'])
+
+
+@celery.app.task(
+    name='federation.scan_library_page',
+    autoretry_for=[RequestException],
+    retry_backoff=30,
+    max_retries=5)
+@celery.require_instance(models.Library, 'library')
+def scan_library_page(library, page_url, until=None):
+    if not library.federation_enabled:
+        return
+
+    data = lb.get_library_page(library, page_url)
+    lts = []
+    for item_serializer in data['items']:
+        item_date = item_serializer.validated_data['published']
+        if until and item_date < until:
+            return
+        lts.append(item_serializer.save())
+
+    next_page = data.get('next')
+    if next_page and next_page != page_url:
+        scan_library_page.delay(library_id=library.id, page_url=next_page)
+
+
+@celery.app.task(name='federation.clean_music_cache')
+def clean_music_cache():
+    preferences = global_preferences_registry.manager()
+    delay = preferences['federation__music_cache_duration']
+    if delay < 1:
+        return  # cache clearing disabled
+
+    candidates = models.LibraryTrack.objects.filter(
+        audio_file__isnull=False
+    ).values_list('local_track_file__track', flat=True)
+    listenings = Listening.objects.filter(
+        creation_date__gte=timezone.now() - datetime.timedelta(minutes=delay),
+        track__pk__in=candidates).values_list('track', flat=True)
+    too_old = set(candidates) - set(listenings)
+
+    to_remove = models.LibraryTrack.objects.filter(
+        local_track_file__track__pk__in=too_old).only('audio_file')
+    for lt in to_remove:
+        lt.audio_file.delete()
diff --git a/api/funkwhale_api/federation/urls.py b/api/funkwhale_api/federation/urls.py
index f2c6f4c78c61973436b3d92aacebdec3506156fd..2c24b5257e1937ecd8e82f13766bee238705f2ca 100644
--- a/api/funkwhale_api/federation/urls.py
+++ b/api/funkwhale_api/federation/urls.py
@@ -1,8 +1,10 @@
-from rest_framework import routers
+from django.conf.urls import include, url
 
+from rest_framework import routers
 from . import views
 
 router = routers.SimpleRouter(trailing_slash=False)
+music_router = routers.SimpleRouter(trailing_slash=False)
 router.register(
     r'federation/instance/actors',
     views.InstanceActorViewSet,
@@ -12,4 +14,11 @@ router.register(
     views.WellKnownViewSet,
     'well-known')
 
-urlpatterns = router.urls
+music_router.register(
+    r'files',
+    views.MusicFilesViewSet,
+    'files',
+)
+urlpatterns = router.urls + [
+    url('federation/music/', include((music_router.urls, 'music'), namespace='music'))
+]
diff --git a/api/funkwhale_api/federation/views.py b/api/funkwhale_api/federation/views.py
index 2e3feb8d082ebdca00917e59e13d5f3cc601eb33..381f87eff2e90f4a64feade856756044393d4f4d 100644
--- a/api/funkwhale_api/federation/views.py
+++ b/api/funkwhale_api/federation/views.py
@@ -1,16 +1,33 @@
 from django import forms
 from django.conf import settings
+from django.core import paginator
+from django.db import transaction
 from django.http import HttpResponse
+from django.urls import reverse
 
-from rest_framework import viewsets
-from rest_framework import views
+from rest_framework import mixins
+from rest_framework import permissions as rest_permissions
 from rest_framework import response
+from rest_framework import views
+from rest_framework import viewsets
 from rest_framework.decorators import list_route, detail_route
+from rest_framework.serializers import ValidationError
+
+from funkwhale_api.common import utils as funkwhale_utils
+from funkwhale_api.common.permissions import HasModelPermission
+from funkwhale_api.music.models import TrackFile
 
+from . import activity
 from . import actors
 from . import authentication
+from . import filters
+from . import library
+from . import models
+from . import permissions
 from . import renderers
 from . import serializers
+from . import tasks
+from . import utils
 from . import webfinger
 
 
@@ -38,8 +55,8 @@ class InstanceActorViewSet(FederationMixin, viewsets.GenericViewSet):
     def retrieve(self, request, *args, **kwargs):
         system_actor = self.get_object()
         actor = system_actor.get_actor_instance()
-        serializer = serializers.ActorSerializer(actor)
-        return response.Response(serializer.data, status=200)
+        data = actor.system_conf.serialize()
+        return response.Response(data, status=200)
 
     @detail_route(methods=['get', 'post'])
     def inbox(self, request, *args, **kwargs):
@@ -52,7 +69,7 @@ class InstanceActorViewSet(FederationMixin, viewsets.GenericViewSet):
             data = handler(request.data, actor=request.actor)
         except NotImplementedError:
             return response.Response(status=405)
-        return response.Response(data, status=200)
+        return response.Response({}, status=200)
 
     @detail_route(methods=['get', 'post'])
     def outbox(self, request, *args, **kwargs):
@@ -64,7 +81,7 @@ class InstanceActorViewSet(FederationMixin, viewsets.GenericViewSet):
             data = handler(request.data, actor=request.actor)
         except NotImplementedError:
             return response.Response(status=405)
-        return response.Response(data, status=200)
+        return response.Response({}, status=200)
 
 
 class WellKnownViewSet(FederationMixin, viewsets.GenericViewSet):
@@ -101,3 +118,174 @@ class WellKnownViewSet(FederationMixin, viewsets.GenericViewSet):
         username, hostname = clean_result
         actor = actors.SYSTEM_ACTORS[username].get_actor_instance()
         return serializers.ActorWebfingerSerializer(actor).data
+
+
+class MusicFilesViewSet(FederationMixin, viewsets.GenericViewSet):
+    authentication_classes = [
+        authentication.SignatureAuthentication]
+    permission_classes = [permissions.LibraryFollower]
+    renderer_classes = [renderers.ActivityPubRenderer]
+
+    def list(self, request, *args, **kwargs):
+        page = request.GET.get('page')
+        library = actors.SYSTEM_ACTORS['library'].get_actor_instance()
+        qs = TrackFile.objects.order_by('-creation_date').select_related(
+            'track__artist',
+            'track__album__artist'
+        ).filter(library_track__isnull=True)
+        if page is None:
+            conf = {
+                'id': utils.full_url(reverse('federation:music:files-list')),
+                'page_size': settings.FEDERATION_COLLECTION_PAGE_SIZE,
+                'items': qs,
+                'item_serializer': serializers.AudioSerializer,
+                'actor': library,
+            }
+            serializer = serializers.PaginatedCollectionSerializer(conf)
+            data = serializer.data
+        else:
+            try:
+                page_number = int(page)
+            except:
+                return response.Response(
+                    {'page': ['Invalid page number']}, status=400)
+            p = paginator.Paginator(
+                qs, settings.FEDERATION_COLLECTION_PAGE_SIZE)
+            try:
+                page = p.page(page_number)
+                conf = {
+                    'id': utils.full_url(reverse('federation:music:files-list')),
+                    'page': page,
+                    'item_serializer': serializers.AudioSerializer,
+                    'actor': library,
+                }
+                serializer = serializers.CollectionPageSerializer(conf)
+                data = serializer.data
+            except paginator.EmptyPage:
+                return response.Response(status=404)
+
+        return response.Response(data)
+
+
+class LibraryPermission(HasModelPermission):
+    model = models.Library
+
+
+class LibraryViewSet(
+        mixins.RetrieveModelMixin,
+        mixins.UpdateModelMixin,
+        mixins.ListModelMixin,
+        viewsets.GenericViewSet):
+    permission_classes = [LibraryPermission]
+    queryset = models.Library.objects.all().select_related(
+        'actor',
+        'follow',
+    )
+    lookup_field = 'uuid'
+    filter_class = filters.LibraryFilter
+    serializer_class = serializers.APILibrarySerializer
+    ordering_fields = (
+        'id',
+        'creation_date',
+        'fetched_date',
+        'actor__domain',
+        'tracks_count',
+    )
+
+    @list_route(methods=['get'])
+    def fetch(self, request, *args, **kwargs):
+        account = request.GET.get('account')
+        if not account:
+            return response.Response(
+                {'account': 'This field is mandatory'}, status=400)
+
+        data = library.scan_from_account_name(account)
+        return response.Response(data)
+
+    @detail_route(methods=['post'])
+    def scan(self, request, *args, **kwargs):
+        library = self.get_object()
+        serializer = serializers.APILibraryScanSerializer(
+            data=request.data
+        )
+        serializer.is_valid(raise_exception=True)
+        result = tasks.scan_library.delay(
+            library_id=library.pk,
+            until=serializer.validated_data.get('until')
+        )
+        return response.Response({'task': result.id})
+
+    @list_route(methods=['get'])
+    def following(self, request, *args, **kwargs):
+        library_actor = actors.SYSTEM_ACTORS['library'].get_actor_instance()
+        queryset = models.Follow.objects.filter(
+            actor=library_actor
+        ).select_related(
+            'actor',
+            'target',
+        ).order_by('-creation_date')
+        filterset = filters.FollowFilter(request.GET, queryset=queryset)
+        final_qs = filterset.qs
+        serializer = serializers.APIFollowSerializer(final_qs, many=True)
+        data = {
+            'results': serializer.data,
+            'count': len(final_qs),
+        }
+        return response.Response(data)
+
+    @list_route(methods=['get', 'patch'])
+    def followers(self, request, *args, **kwargs):
+        if request.method.lower() == 'patch':
+            serializer = serializers.APILibraryFollowUpdateSerializer(
+                data=request.data)
+            serializer.is_valid(raise_exception=True)
+            follow = serializer.save()
+            return response.Response(
+                serializers.APIFollowSerializer(follow).data
+            )
+
+        library_actor = actors.SYSTEM_ACTORS['library'].get_actor_instance()
+        queryset = models.Follow.objects.filter(
+            target=library_actor
+        ).select_related(
+            'actor',
+            'target',
+        ).order_by('-creation_date')
+        filterset = filters.FollowFilter(request.GET, queryset=queryset)
+        final_qs = filterset.qs
+        serializer = serializers.APIFollowSerializer(final_qs, many=True)
+        data = {
+            'results': serializer.data,
+            'count': len(final_qs),
+        }
+        return response.Response(data)
+
+    @transaction.atomic
+    def create(self, request, *args, **kwargs):
+        serializer = serializers.APILibraryCreateSerializer(data=request.data)
+        serializer.is_valid(raise_exception=True)
+        library = serializer.save()
+        return response.Response(serializer.data, status=201)
+
+
+class LibraryTrackViewSet(
+        mixins.ListModelMixin,
+        viewsets.GenericViewSet):
+    permission_classes = [LibraryPermission]
+    queryset = models.LibraryTrack.objects.all().select_related(
+        'library__actor',
+        'library__follow',
+        'local_track_file',
+    )
+    filter_class = filters.LibraryTrackFilter
+    serializer_class = serializers.APILibraryTrackSerializer
+    ordering_fields = (
+        'id',
+        'artist_name',
+        'title',
+        'album_title',
+        'creation_date',
+        'modification_date',
+        'fetched_date',
+        'published_date',
+    )
diff --git a/api/funkwhale_api/federation/webfinger.py b/api/funkwhale_api/federation/webfinger.py
index 4e97533852421210aead271aa510c40d29478e6e..f5cb996359fdfcd072d4715c06318230596692fa 100644
--- a/api/funkwhale_api/federation/webfinger.py
+++ b/api/funkwhale_api/federation/webfinger.py
@@ -2,8 +2,11 @@ from django import forms
 from django.conf import settings
 from django.urls import reverse
 
+from funkwhale_api.common import session
+
 from . import actors
 from . import utils
+from . import serializers
 
 VALID_RESOURCE_TYPES = ['acct']
 
@@ -23,17 +26,32 @@ def clean_resource(resource_string):
     return resource_type, resource
 
 
-def clean_acct(acct_string):
+def clean_acct(acct_string, ensure_local=True):
     try:
         username, hostname = acct_string.split('@')
     except ValueError:
         raise forms.ValidationError('Invalid format')
 
-    if hostname.lower() != settings.FEDERATION_HOSTNAME:
+    if ensure_local and hostname.lower() != settings.FEDERATION_HOSTNAME:
         raise forms.ValidationError(
             'Invalid hostname {}'.format(hostname))
 
-    if username not in actors.SYSTEM_ACTORS:
+    if ensure_local and username not in actors.SYSTEM_ACTORS:
         raise forms.ValidationError('Invalid username')
 
     return username, hostname
+
+
+def get_resource(resource_string):
+    resource_type, resource = clean_resource(resource_string)
+    username, hostname = clean_acct(resource, ensure_local=False)
+    url = 'https://{}/.well-known/webfinger?resource={}'.format(
+        hostname, resource_string)
+    response = session.get_session().get(
+        url,
+        verify=settings.EXTERNAL_REQUESTS_VERIFY_SSL,
+        timeout=5)
+    response.raise_for_status()
+    serializer = serializers.ActorWebfingerSerializer(data=response.json())
+    serializer.is_valid(raise_exception=True)
+    return serializer.validated_data
diff --git a/api/funkwhale_api/music/factories.py b/api/funkwhale_api/music/factories.py
index 303e45228e2d851708d9bb2404dd19e01026918a..ea7ff64dfa0cb154c871089e75c70cf9c19a6e1a 100644
--- a/api/funkwhale_api/music/factories.py
+++ b/api/funkwhale_api/music/factories.py
@@ -2,6 +2,9 @@ import factory
 import os
 
 from funkwhale_api.factories import registry, ManyToManyFromList
+from funkwhale_api.federation.factories import (
+    LibraryTrackFactory,
+)
 from funkwhale_api.users.factories import UserFactory
 
 SAMPLES_PATH = os.path.join(
@@ -53,6 +56,18 @@ class TrackFileFactory(factory.django.DjangoModelFactory):
     class Meta:
         model = 'music.TrackFile'
 
+    class Params:
+        federation = factory.Trait(
+            audio_file=None,
+            library_track=factory.SubFactory(LibraryTrackFactory),
+            mimetype=factory.LazyAttribute(
+                lambda o: o.library_track.audio_mimetype
+            ),
+            source=factory.LazyAttribute(
+                lambda o: o.library_track.audio_url
+            ),
+        )
+
 
 @registry.register
 class ImportBatchFactory(factory.django.DjangoModelFactory):
@@ -61,6 +76,15 @@ class ImportBatchFactory(factory.django.DjangoModelFactory):
     class Meta:
         model = 'music.ImportBatch'
 
+    class Params:
+        federation = factory.Trait(
+            submitted_by=None,
+            source='federation',
+        )
+        finished = factory.Trait(
+            status='finished',
+        )
+
 
 @registry.register
 class ImportJobFactory(factory.django.DjangoModelFactory):
@@ -71,6 +95,17 @@ class ImportJobFactory(factory.django.DjangoModelFactory):
     class Meta:
         model = 'music.ImportJob'
 
+    class Params:
+        federation = factory.Trait(
+            mbid=None,
+            library_track=factory.SubFactory(LibraryTrackFactory),
+            batch=factory.SubFactory(ImportBatchFactory, federation=True),
+        )
+        finished = factory.Trait(
+            status='finished',
+            track_file=factory.SubFactory(TrackFileFactory),
+        )
+
 
 @registry.register(name='music.FileImportJob')
 class FileImportJobFactory(ImportJobFactory):
diff --git a/api/funkwhale_api/music/forms.py b/api/funkwhale_api/music/forms.py
index 04e4bfe057c0723b161265c63000a2494c687a9d..e68ab73cc2b95030d6dfb284a10b8becbefc9a9e 100644
--- a/api/funkwhale_api/music/forms.py
+++ b/api/funkwhale_api/music/forms.py
@@ -19,5 +19,5 @@ class TranscodeForm(forms.Form):
         choices=BITRATE_CHOICES, required=False)
 
     track_file = forms.ModelChoiceField(
-        queryset=models.TrackFile.objects.all()
+        queryset=models.TrackFile.objects.exclude(audio_file__isnull=True)
     )
diff --git a/api/funkwhale_api/music/migrations/0023_auto_20180407_1010.py b/api/funkwhale_api/music/migrations/0023_auto_20180407_1010.py
new file mode 100644
index 0000000000000000000000000000000000000000..ed7404ac4f396b32fc43e0879391649333ae52ce
--- /dev/null
+++ b/api/funkwhale_api/music/migrations/0023_auto_20180407_1010.py
@@ -0,0 +1,87 @@
+# Generated by Django 2.0.3 on 2018-04-07 10:10
+
+from django.conf import settings
+from django.db import migrations, models
+import django.db.models.deletion
+import django.utils.timezone
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('federation', '0003_auto_20180407_1010'),
+        ('music', '0022_importbatch_import_request'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='album',
+            name='uuid',
+            field=models.UUIDField(db_index=True, null=True, unique=True),
+        ),
+        migrations.AddField(
+            model_name='artist',
+            name='uuid',
+            field=models.UUIDField(db_index=True, null=True, unique=True),
+        ),
+        migrations.AddField(
+            model_name='importbatch',
+            name='uuid',
+            field=models.UUIDField(db_index=True, null=True, unique=True),
+        ),
+        migrations.AddField(
+            model_name='importjob',
+            name='library_track',
+            field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='import_jobs', to='federation.LibraryTrack'),
+        ),
+        migrations.AddField(
+            model_name='importjob',
+            name='uuid',
+            field=models.UUIDField(db_index=True, null=True, unique=True),
+        ),
+        migrations.AddField(
+            model_name='lyrics',
+            name='uuid',
+            field=models.UUIDField(db_index=True, null=True, unique=True),
+        ),
+        migrations.AddField(
+            model_name='track',
+            name='uuid',
+            field=models.UUIDField(db_index=True, null=True, unique=True),
+        ),
+        migrations.AddField(
+            model_name='trackfile',
+            name='creation_date',
+            field=models.DateTimeField(default=django.utils.timezone.now),
+        ),
+        migrations.AddField(
+            model_name='trackfile',
+            name='library_track',
+            field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='local_track_file', to='federation.LibraryTrack'),
+        ),
+        migrations.AddField(
+            model_name='trackfile',
+            name='modification_date',
+            field=models.DateTimeField(auto_now=True),
+        ),
+        migrations.AddField(
+            model_name='trackfile',
+            name='uuid',
+            field=models.UUIDField(db_index=True, null=True, unique=True),
+        ),
+        migrations.AddField(
+            model_name='work',
+            name='uuid',
+            field=models.UUIDField(db_index=True, null=True, unique=True),
+        ),
+        migrations.AlterField(
+            model_name='importbatch',
+            name='source',
+            field=models.CharField(choices=[('api', 'api'), ('shell', 'shell'), ('federation', 'federation')], default='api', max_length=30),
+        ),
+        migrations.AlterField(
+            model_name='importbatch',
+            name='submitted_by',
+            field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='imports', to=settings.AUTH_USER_MODEL),
+        ),
+    ]
diff --git a/api/funkwhale_api/music/migrations/0024_populate_uuid.py b/api/funkwhale_api/music/migrations/0024_populate_uuid.py
new file mode 100644
index 0000000000000000000000000000000000000000..10c78a3db03a9b3be2b8fca24629ae5e58bcd558
--- /dev/null
+++ b/api/funkwhale_api/music/migrations/0024_populate_uuid.py
@@ -0,0 +1,80 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+import os
+import uuid
+from django.db import migrations, models
+
+
+def populate_uuids(apps, schema_editor):
+    models = [
+        'Album',
+        'Artist',
+        'Importbatch',
+        'Importjob',
+        'Lyrics',
+        'Track',
+        'Trackfile',
+        'Work',
+    ]
+    for m in models:
+        kls = apps.get_model('music', m)
+        qs = kls.objects.filter(uuid__isnull=True).only('id')
+        print('Setting uuids for {} ({} objects)'.format(m, len(qs)))
+        for o in qs:
+            o.uuid = uuid.uuid4()
+            o.save(update_fields=['uuid'])
+
+
+def rewind(apps, schema_editor):
+    pass
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('music', '0023_auto_20180407_1010'),
+    ]
+
+    operations = [
+        migrations.RunPython(populate_uuids, rewind),
+        migrations.AlterField(
+            model_name='album',
+            name='uuid',
+            field=models.UUIDField(db_index=True, default=uuid.uuid4, unique=True),
+        ),
+        migrations.AlterField(
+            model_name='artist',
+            name='uuid',
+            field=models.UUIDField(db_index=True, default=uuid.uuid4, unique=True),
+        ),
+        migrations.AlterField(
+            model_name='importbatch',
+            name='uuid',
+            field=models.UUIDField(db_index=True, default=uuid.uuid4, unique=True),
+        ),
+        migrations.AlterField(
+            model_name='importjob',
+            name='uuid',
+            field=models.UUIDField(db_index=True, default=uuid.uuid4, unique=True),
+        ),
+        migrations.AlterField(
+            model_name='lyrics',
+            name='uuid',
+            field=models.UUIDField(db_index=True, default=uuid.uuid4, unique=True),
+        ),
+        migrations.AlterField(
+            model_name='track',
+            name='uuid',
+            field=models.UUIDField(db_index=True, default=uuid.uuid4, unique=True),
+        ),
+        migrations.AlterField(
+            model_name='trackfile',
+            name='uuid',
+            field=models.UUIDField(db_index=True, default=uuid.uuid4, unique=True),
+        ),
+        migrations.AlterField(
+            model_name='work',
+            name='uuid',
+            field=models.UUIDField(db_index=True, default=uuid.uuid4, unique=True),
+        ),
+    ]
diff --git a/api/funkwhale_api/music/models.py b/api/funkwhale_api/music/models.py
index 7138dcdd6d51f52568649d06fc34eb655d64e4d0..4ec3ff4274efca8c6ad5f2e370ce90eda6b10141 100644
--- a/api/funkwhale_api/music/models.py
+++ b/api/funkwhale_api/music/models.py
@@ -5,6 +5,7 @@ import datetime
 import tempfile
 import shutil
 import markdown
+import uuid
 
 from django.conf import settings
 from django.db import models
@@ -20,12 +21,15 @@ from versatileimagefield.fields import VersatileImageField
 
 from funkwhale_api import downloader
 from funkwhale_api import musicbrainz
+from funkwhale_api.federation import utils as federation_utils
 from . import importers
 from . import utils
 
 
 class APIModelMixin(models.Model):
     mbid = models.UUIDField(unique=True, db_index=True, null=True, blank=True)
+    uuid = models.UUIDField(
+        unique=True, db_index=True, default=uuid.uuid4)
     api_includes = []
     creation_date = models.DateTimeField(default=timezone.now)
     import_hooks = []
@@ -65,6 +69,13 @@ class APIModelMixin(models.Model):
                 pass
         return cleaned_data
 
+    @property
+    def musicbrainz_url(self):
+        if self.mbid:
+            return 'https://musicbrainz.org/{}/{}'.format(
+                self.musicbrainz_model, self.mbid)
+
+
 class Artist(APIModelMixin):
     name = models.CharField(max_length=255)
 
@@ -90,10 +101,19 @@ class Artist(APIModelMixin):
                 t.append(tag)
         return set(t)
 
+    @classmethod
+    def get_or_create_from_name(cls, name, **kwargs):
+        kwargs.update({'name': name})
+        return cls.objects.get_or_create(
+            name__iexact=name,
+            defaults=kwargs)[0]
+
+
 def import_artist(v):
     a = Artist.get_or_create_from_api(mbid=v[0]['artist']['id'])[0]
     return a
 
+
 def parse_date(v):
     if len(v) == 4:
         return datetime.date(int(v), 1, 1)
@@ -108,6 +128,7 @@ def import_tracks(instance, cleaned_data, raw_data):
         track_cleaned_data['position'] = int(track_data['position'])
         track = importers.load(Track, track_cleaned_data, track_data, Track.import_hooks)
 
+
 class Album(APIModelMixin):
     title = models.CharField(max_length=255)
     artist = models.ForeignKey(
@@ -170,6 +191,14 @@ class Album(APIModelMixin):
                 t.append(tag)
         return set(t)
 
+    @classmethod
+    def get_or_create_from_title(cls, title, **kwargs):
+        kwargs.update({'title': title})
+        return cls.objects.get_or_create(
+            title__iexact=title,
+            defaults=kwargs)[0]
+
+
 def import_tags(instance, cleaned_data, raw_data):
     MINIMUM_COUNT = 2
     tags_to_add = []
@@ -182,6 +211,7 @@ def import_tags(instance, cleaned_data, raw_data):
         tags_to_add.append(tag_data['name'])
     instance.tags.add(*tags_to_add)
 
+
 def import_album(v):
     a = Album.get_or_create_from_api(mbid=v[0]['id'])[0]
     return a
@@ -248,6 +278,8 @@ class Work(APIModelMixin):
 
 
 class Lyrics(models.Model):
+    uuid = models.UUIDField(
+        unique=True, db_index=True, default=uuid.uuid4)
     work = models.ForeignKey(
         Work,
         related_name='lyrics',
@@ -328,7 +360,7 @@ class Track(APIModelMixin):
     def save(self, **kwargs):
         try:
             self.artist
-        except  Artist.DoesNotExist:
+        except Artist.DoesNotExist:
             self.artist = self.album.artist
         super().save(**kwargs)
 
@@ -366,16 +398,35 @@ class Track(APIModelMixin):
                 self.mbid)
         return settings.FUNKWHALE_URL + '/tracks/{}'.format(self.pk)
 
+    @classmethod
+    def get_or_create_from_title(cls, title, **kwargs):
+        kwargs.update({'title': title})
+        return cls.objects.get_or_create(
+            title__iexact=title,
+            defaults=kwargs)[0]
+
 
 class TrackFile(models.Model):
+    uuid = models.UUIDField(
+        unique=True, db_index=True, default=uuid.uuid4)
     track = models.ForeignKey(
         Track, related_name='files', on_delete=models.CASCADE)
     audio_file = models.FileField(upload_to='tracks/%Y/%m/%d', max_length=255)
     source = models.URLField(null=True, blank=True)
+    creation_date = models.DateTimeField(default=timezone.now)
+    modification_date = models.DateTimeField(auto_now=True)
     duration = models.IntegerField(null=True, blank=True)
     acoustid_track_id = models.UUIDField(null=True, blank=True)
     mimetype = models.CharField(null=True, blank=True, max_length=200)
 
+    library_track = models.OneToOneField(
+        'federation.LibraryTrack',
+        related_name='local_track_file',
+        on_delete=models.CASCADE,
+        null=True,
+        blank=True,
+    )
+
     def download_file(self):
         # import the track file, since there is not any
         # we create a tmp dir for the download
@@ -391,12 +442,15 @@ class TrackFile(models.Model):
         shutil.rmtree(tmp_dir)
         return self.audio_file
 
+    def get_federation_url(self):
+        return federation_utils.full_url(
+            '/federation/music/file/{}'.format(self.uuid)
+        )
+
     @property
     def path(self):
-        if settings.PROTECT_AUDIO_FILES:
-            return reverse(
-                'api:v1:trackfiles-serve', kwargs={'pk': self.pk})
-        return self.audio_file.url
+        return reverse(
+            'api:v1:trackfiles-serve', kwargs={'pk': self.pk})
 
     @property
     def filename(self):
@@ -417,10 +471,14 @@ IMPORT_STATUS_CHOICES = (
     ('skipped', 'Skipped'),
 )
 
+
 class ImportBatch(models.Model):
+    uuid = models.UUIDField(
+        unique=True, db_index=True, default=uuid.uuid4)
     IMPORT_BATCH_SOURCES = [
         ('api', 'api'),
-        ('shell', 'shell')
+        ('shell', 'shell'),
+        ('federation', 'federation'),
     ]
     source = models.CharField(
         max_length=30, default='api', choices=IMPORT_BATCH_SOURCES)
@@ -428,6 +486,8 @@ class ImportBatch(models.Model):
     submitted_by = models.ForeignKey(
         'users.User',
         related_name='imports',
+        null=True,
+        blank=True,
         on_delete=models.CASCADE)
     status = models.CharField(
         choices=IMPORT_STATUS_CHOICES, default='pending', max_length=30)
@@ -437,6 +497,7 @@ class ImportBatch(models.Model):
         null=True,
         blank=True,
         on_delete=models.CASCADE)
+
     class Meta:
         ordering = ['-creation_date']
 
@@ -444,11 +505,22 @@ class ImportBatch(models.Model):
         return str(self.pk)
 
     def update_status(self):
+        old_status = self.status
         self.status = utils.compute_status(self.jobs.all())
         self.save(update_fields=['status'])
+        if self.status != old_status and self.status == 'finished':
+            from . import tasks
+            tasks.import_batch_notify_followers.delay(import_batch_id=self.pk)
+
+    def get_federation_url(self):
+        return federation_utils.full_url(
+            '/federation/music/import/batch/{}'.format(self.uuid)
+        )
 
 
 class ImportJob(models.Model):
+    uuid = models.UUIDField(
+        unique=True, db_index=True, default=uuid.uuid4)
     batch = models.ForeignKey(
         ImportBatch, related_name='jobs', on_delete=models.CASCADE)
     track_file = models.ForeignKey(
@@ -465,6 +537,14 @@ class ImportJob(models.Model):
     audio_file = models.FileField(
         upload_to='imports/%Y/%m/%d', max_length=255, null=True, blank=True)
 
+    library_track = models.ForeignKey(
+        'federation.LibraryTrack',
+        related_name='import_jobs',
+        on_delete=models.SET_NULL,
+        null=True,
+        blank=True
+    )
+
     class Meta:
         ordering = ('id', )
 
diff --git a/api/funkwhale_api/music/permissions.py b/api/funkwhale_api/music/permissions.py
new file mode 100644
index 0000000000000000000000000000000000000000..61fc65bebf523f916483e67e2c7a311588bfc6de
--- /dev/null
+++ b/api/funkwhale_api/music/permissions.py
@@ -0,0 +1,28 @@
+from django.conf import settings
+
+from rest_framework.permissions import BasePermission
+
+from funkwhale_api.federation import actors
+from funkwhale_api.federation import models
+
+
+class Listen(BasePermission):
+
+    def has_permission(self, request, view):
+        if not settings.PROTECT_AUDIO_FILES:
+            return True
+
+        user = getattr(request, 'user', None)
+        if user and user.is_authenticated:
+            return True
+
+        actor = getattr(request, 'actor', None)
+        if actor is None:
+            return False
+
+        library = actors.SYSTEM_ACTORS['library'].get_actor_instance()
+        return models.Follow.objects.filter(
+            target=library,
+            actor=actor,
+            approved=True
+        ).exists()
diff --git a/api/funkwhale_api/music/serializers.py b/api/funkwhale_api/music/serializers.py
index 48419bbe45675aad4e6ad271e9be420219f7d671..b5f69eb1db866225fca4cc147e71a08901194dd8 100644
--- a/api/funkwhale_api/music/serializers.py
+++ b/api/funkwhale_api/music/serializers.py
@@ -1,7 +1,11 @@
+from django.db import transaction
 from rest_framework import serializers
 from taggit.models import Tag
 
 from funkwhale_api.activity import serializers as activity_serializers
+from funkwhale_api.federation import utils as federation_utils
+from funkwhale_api.federation.models import LibraryTrack
+from funkwhale_api.federation.serializers import AP_CONTEXT
 
 from . import models
 
@@ -150,3 +154,25 @@ class TrackActivitySerializer(activity_serializers.ModelSerializer):
 
     def get_type(self, obj):
         return 'Audio'
+
+
+class SubmitFederationTracksSerializer(serializers.Serializer):
+    library_tracks = serializers.PrimaryKeyRelatedField(
+        many=True,
+        queryset=LibraryTrack.objects.filter(local_track_file__isnull=True),
+    )
+
+    @transaction.atomic
+    def save(self, **kwargs):
+        batch = models.ImportBatch.objects.create(
+            source='federation',
+            **kwargs
+        )
+        for lt in self.validated_data['library_tracks']:
+            models.ImportJob.objects.create(
+                batch=batch,
+                library_track=lt,
+                mbid=lt.mbid,
+                source=lt.url,
+            )
+        return batch
diff --git a/api/funkwhale_api/music/tasks.py b/api/funkwhale_api/music/tasks.py
index bf7a847d0c45a2aeabd7fb189ae017a655e4e966..bc5ab94f0ae7a56470f42e3c705c40ff11d86053 100644
--- a/api/funkwhale_api/music/tasks.py
+++ b/api/funkwhale_api/music/tasks.py
@@ -2,6 +2,10 @@ from django.core.files.base import ContentFile
 
 from dynamic_preferences.registries import global_preferences_registry
 
+from funkwhale_api.federation import activity
+from funkwhale_api.federation import actors
+from funkwhale_api.federation import models as federation_models
+from funkwhale_api.federation import serializers as federation_serializers
 from funkwhale_api.taskapp import celery
 from funkwhale_api.providers.acoustid import get_acoustid_client
 from funkwhale_api.providers.audiofile.tasks import import_track_data_from_path
@@ -25,6 +29,48 @@ def set_acoustid_on_track_file(track_file):
         return update(result['id'])
 
 
+def import_track_from_remote(library_track):
+    metadata = library_track.metadata
+    try:
+        track_mbid = metadata['recording']['musicbrainz_id']
+        assert track_mbid  # for null/empty values
+    except (KeyError, AssertionError):
+        pass
+    else:
+        return models.Track.get_or_create_from_api(mbid=track_mbid)
+
+    try:
+        album_mbid = metadata['release']['musicbrainz_id']
+        assert album_mbid  # for null/empty values
+    except (KeyError, AssertionError):
+        pass
+    else:
+        album = models.Album.get_or_create_from_api(mbid=album_mbid)
+        return models.Track.get_or_create_from_title(
+            library_track.title, artist=album.artist, album=album)
+
+    try:
+        artist_mbid = metadata['artist']['musicbrainz_id']
+        assert artist_mbid  # for null/empty values
+    except (KeyError, AssertionError):
+        pass
+    else:
+        artist = models.Artist.get_or_create_from_api(mbid=artist_mbid)
+        album = models.Album.get_or_create_from_title(
+            library_track.album_title, artist=artist)
+        return models.Track.get_or_create_from_title(
+            library_track.title, artist=artist, album=album)
+
+    # worst case scenario, we have absolutely no way to link to a
+    # musicbrainz resource, we rely on the name/titles
+    artist = models.Artist.get_or_create_from_name(
+        library_track.artist_name)
+    album = models.Album.get_or_create_from_title(
+        library_track.album_title, artist=artist)
+    return models.Track.get_or_create_from_title(
+        library_track.title, artist=artist, album=album)
+
+
 def _do_import(import_job, replace, use_acoustid=True):
     from_file = bool(import_job.audio_file)
     mbid = import_job.mbid
@@ -43,8 +89,14 @@ def _do_import(import_job, replace, use_acoustid=True):
             acoustid_track_id = match['id']
     if mbid:
         track, _ = models.Track.get_or_create_from_api(mbid=mbid)
-    else:
+    elif import_job.audio_file:
         track = import_track_data_from_path(import_job.audio_file.path)
+    elif import_job.library_track:
+        track = import_track_from_remote(import_job.library_track)
+    else:
+        raise ValueError(
+            'Not enough data to process import, '
+            'add a mbid, an audio file or a library track')
 
     track_file = None
     if replace:
@@ -63,6 +115,14 @@ def _do_import(import_job, replace, use_acoustid=True):
         track_file.audio_file = ContentFile(import_job.audio_file.read())
         track_file.audio_file.name = import_job.audio_file.name
         track_file.duration = duration
+    elif import_job.library_track:
+        track_file.library_track = import_job.library_track
+        track_file.mimetype = import_job.library_track.audio_mimetype
+        if import_job.library_track.library.download_files:
+            raise NotImplementedError()
+        else:
+            # no downloading, we hotlink
+            pass
     else:
         track_file.download_file()
     track_file.save()
@@ -72,6 +132,7 @@ def _do_import(import_job, replace, use_acoustid=True):
         # it's imported on the track, we don't need it anymore
         import_job.audio_file.delete()
     import_job.save()
+
     return track.pk
 
 
@@ -106,3 +167,44 @@ def fetch_content(lyrics):
     cleaned_content = lyrics_utils.clean_content(content)
     lyrics.content = cleaned_content
     lyrics.save(update_fields=['content'])
+
+
+@celery.app.task(name='music.import_batch_notify_followers')
+@celery.require_instance(
+    models.ImportBatch.objects.filter(status='finished'), 'import_batch')
+def import_batch_notify_followers(import_batch):
+    if not settings.FEDERATION_ENABLED:
+        return
+
+    if import_batch.source == 'federation':
+        return
+
+    library_actor = actors.SYSTEM_ACTORS['library'].get_actor_instance()
+    followers = library_actor.get_approved_followers()
+    jobs = import_batch.jobs.filter(
+        status='finished',
+        library_track__isnull=True,
+        track_file__isnull=False,
+    ).select_related(
+        'track_file__track__artist',
+        'track_file__track__album__artist',
+    )
+    track_files = [job.track_file for job in jobs]
+    collection = federation_serializers.CollectionSerializer({
+        'actor': library_actor,
+        'id': import_batch.get_federation_url(),
+        'items': track_files,
+        'item_serializer': federation_serializers.AudioSerializer
+    }).data
+    for f in followers:
+        create = federation_serializers.ActivitySerializer(
+            {
+                'type': 'Create',
+                'id': collection['id'],
+                'object': collection,
+                'actor': library_actor.url,
+                'to': [f.url],
+            }
+        ).data
+
+        activity.deliver(create, on_behalf_of=library_actor, to=[f.url])
diff --git a/api/funkwhale_api/music/utils.py b/api/funkwhale_api/music/utils.py
index df659cb8057b3c519610ff0a70f2524b973d220f..af0e59ab497e63ea06be2a50ecf9df0651072587 100644
--- a/api/funkwhale_api/music/utils.py
+++ b/api/funkwhale_api/music/utils.py
@@ -60,3 +60,10 @@ def compute_status(jobs):
     if pending:
         return 'pending'
     return 'finished'
+
+
+def get_ext_from_type(mimetype):
+    mapping = {
+        'audio/ogg': 'ogg',
+        'audio/mpeg': 'mp3',
+    }
diff --git a/api/funkwhale_api/music/views.py b/api/funkwhale_api/music/views.py
index 5ac3143f9e647feee128a07bbed29b18e9766aa7..e8ace1b3ab676c2cc6b76f134dbae6668ac2d655 100644
--- a/api/funkwhale_api/music/views.py
+++ b/api/funkwhale_api/music/views.py
@@ -1,39 +1,47 @@
 import ffmpeg
 import os
 import json
+import logging
 import subprocess
 import unicodedata
 import urllib
 
-from django.urls import reverse
+from django.contrib.auth.decorators import login_required
+from django.core.exceptions import ObjectDoesNotExist
+from django.conf import settings
 from django.db import models, transaction
 from django.db.models.functions import Length
-from django.conf import settings
 from django.http import StreamingHttpResponse
+from django.urls import reverse
+from django.utils.decorators import method_decorator
 
 from rest_framework import viewsets, views, mixins
 from rest_framework.decorators import detail_route, list_route
 from rest_framework.response import Response
+from rest_framework import settings as rest_settings
 from rest_framework import permissions
 from musicbrainzngs import ResponseError
-from django.contrib.auth.decorators import login_required
-from django.utils.decorators import method_decorator
 
 from funkwhale_api.common import utils as funkwhale_utils
+from funkwhale_api.federation import actors
 from funkwhale_api.requests.models import ImportRequest
 from funkwhale_api.musicbrainz import api
 from funkwhale_api.common.permissions import (
     ConditionalAuthentication, HasModelPermission)
 from taggit.models import Tag
+from funkwhale_api.federation.authentication import SignatureAuthentication
 
+from . import filters
 from . import forms
+from . import importers
 from . import models
+from . import permissions as music_permissions
 from . import serializers
-from . import importers
-from . import filters
 from . import tasks
 from . import utils
 
+logger = logging.getLogger(__name__)
+
 
 class SearchMixin(object):
     search_fields = []
@@ -45,6 +53,7 @@ class SearchMixin(object):
         serializer = self.serializer_class(queryset, many=True)
         return Response(serializer.data)
 
+
 class TagViewSetMixin(object):
 
     def get_queryset(self):
@@ -179,22 +188,45 @@ class TrackViewSet(TagViewSetMixin, SearchMixin, viewsets.ReadOnlyModelViewSet):
 class TrackFileViewSet(viewsets.ReadOnlyModelViewSet):
     queryset = (models.TrackFile.objects.all().order_by('-id'))
     serializer_class = serializers.TrackFileSerializer
-    permission_classes = [ConditionalAuthentication]
+    authentication_classes = rest_settings.api_settings.DEFAULT_AUTHENTICATION_CLASSES + [
+        SignatureAuthentication
+    ]
+    permission_classes = [music_permissions.Listen]
 
     @detail_route(methods=['get'])
     def serve(self, request, *args, **kwargs):
         try:
-            f = models.TrackFile.objects.get(pk=kwargs['pk'])
+            f = models.TrackFile.objects.select_related(
+                'library_track',
+                'track__album__artist',
+                'track__artist',
+            ).get(pk=kwargs['pk'])
         except models.TrackFile.DoesNotExist:
             return Response(status=404)
 
+        mt = f.mimetype
+        audio_file = f.audio_file
+        try:
+            library_track = f.library_track
+        except ObjectDoesNotExist:
+            library_track = None
+        if library_track and not audio_file:
+            if not library_track.audio_file:
+                # we need to populate from cache
+                library_track.download_audio()
+            audio_file = library_track.audio_file
+            mt = library_track.audio_mimetype
         response = Response()
-        filename = "filename*=UTF-8''{}".format(
-            urllib.parse.quote(f.filename))
-        response["Content-Disposition"] = "attachment; {}".format(filename)
+        filename = f.filename
         response['X-Accel-Redirect'] = "{}{}".format(
             settings.PROTECT_FILES_PATH,
-            f.audio_file.url)
+            audio_file.url)
+        filename = "filename*=UTF-8''{}".format(
+            urllib.parse.quote(filename))
+        response["Content-Disposition"] = "attachment; {}".format(filename)
+        if mt:
+            response["Content-Type"] = mt
+
         return response
 
     @list_route(methods=['get'])
@@ -208,6 +240,8 @@ class TrackFileViewSet(viewsets.ReadOnlyModelViewSet):
             return Response(form.errors, status=400)
 
         f = form.cleaned_data['track_file']
+        if not f.audio_file:
+            return Response(status=400)
         output_kwargs = {
             'format': form.cleaned_data['to']
         }
@@ -351,6 +385,22 @@ class SubmitViewSet(viewsets.ViewSet):
             data, request, batch=None, import_request=import_request)
         return Response(import_data)
 
+    @list_route(methods=['post'])
+    @transaction.non_atomic_requests
+    def federation(self, request, *args, **kwargs):
+        serializer = serializers.SubmitFederationTracksSerializer(
+            data=request.data)
+        serializer.is_valid(raise_exception=True)
+        batch = serializer.save(submitted_by=request.user)
+        for job in batch.jobs.all():
+            funkwhale_utils.on_commit(
+                tasks.import_job_run.delay,
+                import_job_id=job.pk,
+                use_acoustid=False,
+            )
+
+        return Response({'id': batch.id}, status=201)
+
     @transaction.atomic
     def _import_album(self, data, request, batch=None, import_request=None):
         # we import the whole album here to prevent race conditions that occurs
diff --git a/api/funkwhale_api/musicbrainz/client.py b/api/funkwhale_api/musicbrainz/client.py
index 049ed298c7b22cb6f717ee3b571f1153d47f3c2f..8e7076a78b1ae25f47c521c1360d98f077576813 100644
--- a/api/funkwhale_api/musicbrainz/client.py
+++ b/api/funkwhale_api/musicbrainz/client.py
@@ -5,7 +5,7 @@ from django.conf import settings
 from funkwhale_api import __version__
 
 _api = musicbrainzngs
-_api.set_useragent('funkwhale', str(__version__), 'contact@eliotberriot.com')
+_api.set_useragent('funkwhale', str(__version__), settings.FUNKWHALE_URL)
 
 
 store = memoize.djangocache.Cache('default')
diff --git a/api/funkwhale_api/radios/models.py b/api/funkwhale_api/radios/models.py
index d9c12534c03f779d497a1a9e7ae43e12cc3bb32f..0273b53871b4536a52b34870ef5ba2e1e93ae078 100644
--- a/api/funkwhale_api/radios/models.py
+++ b/api/funkwhale_api/radios/models.py
@@ -4,6 +4,7 @@ from django.core.exceptions import ValidationError
 from django.contrib.postgres.fields import JSONField
 from django.contrib.contenttypes.fields import GenericForeignKey
 from django.contrib.contenttypes.models import ContentType
+from django.core.serializers.json import DjangoJSONEncoder
 
 from funkwhale_api.music.models import Track
 
@@ -23,7 +24,7 @@ class Radio(models.Model):
     creation_date = models.DateTimeField(default=timezone.now)
     is_public = models.BooleanField(default=False)
     version = models.PositiveIntegerField(default=0)
-    config = JSONField()
+    config = JSONField(encoder=DjangoJSONEncoder)
 
     def get_candidates(self):
         return filters.run(self.config)
diff --git a/api/funkwhale_api/users/admin.py b/api/funkwhale_api/users/admin.py
index ac37120159d42e5097a372fbafc90102108955a0..89b67d3df96de05b9ffd8fff9ce027f67f101318 100644
--- a/api/funkwhale_api/users/admin.py
+++ b/api/funkwhale_api/users/admin.py
@@ -36,3 +36,13 @@ class MyUserCreationForm(UserCreationForm):
 class UserAdmin(AuthUserAdmin):
     form = MyUserChangeForm
     add_form = MyUserCreationForm
+    list_display = [
+        'username',
+        'email',
+        'date_joined',
+        'last_login',
+        'privacy_level',
+    ]
+    list_filter = [
+        'privacy_level',
+    ]
diff --git a/api/funkwhale_api/users/models.py b/api/funkwhale_api/users/models.py
index 9516c108f896275837be3161fe80e07dda6273e7..572fa9ddca7c1457db1e6a00956c105874e19c62 100644
--- a/api/funkwhale_api/users/models.py
+++ b/api/funkwhale_api/users/models.py
@@ -31,6 +31,9 @@ class User(AbstractUser):
         'dynamic_preferences.change_globalpreferencemodel': {
             'external_codename': 'settings.change',
         },
+        'federation.change_library': {
+            'external_codename': 'federation.manage',
+        },
     }
 
     privacy_level = fields.get_privacy_field()
diff --git a/api/tests/common/test_session.py b/api/tests/common/test_session.py
new file mode 100644
index 0000000000000000000000000000000000000000..7ff1e660bc74936ce945c968bad919871b14ad21
--- /dev/null
+++ b/api/tests/common/test_session.py
@@ -0,0 +1,18 @@
+import funkwhale_api
+
+from funkwhale_api.common import session
+
+
+def test_get_user_agent(settings):
+    settings.FUNKWHALE_URL = 'https://test.com'
+    'http.rb/3.0.0 (Mastodon/2.2.0; +https://mastodon.eliotberriot.com/)'
+    expected = 'python-requests (funkwhale/{}; +{})'.format(
+        funkwhale_api.__version__,
+        settings.FUNKWHALE_URL
+    )
+    assert session.get_user_agent() == expected
+
+
+def test_get_session():
+    expected = session.get_user_agent()
+    assert session.get_session().headers['User-Agent'] == expected
diff --git a/api/tests/conftest.py b/api/tests/conftest.py
index d5bb565651c4b1282920fa455356db4bf6704c35..64dc394e7eef40663bba19b26bd6896f5a5088b1 100644
--- a/api/tests/conftest.py
+++ b/api/tests/conftest.py
@@ -69,6 +69,11 @@ def tmpdir():
     shutil.rmtree(d)
 
 
+@pytest.fixture
+def tmpfile():
+    yield tempfile.NamedTemporaryFile()
+
+
 @pytest.fixture
 def logged_in_client(db, factories, client):
     user = factories['users.User']()
@@ -162,3 +167,12 @@ def media_root(settings):
 def r_mock():
     with requests_mock.mock() as m:
         yield m
+
+
+@pytest.fixture
+def authenticated_actor(factories, mocker):
+    actor = factories['federation.Actor']()
+    mocker.patch(
+        'funkwhale_api.federation.authentication.SignatureAuthentication.authenticate_actor',
+        return_value=actor)
+    yield actor
diff --git a/api/tests/federation/conftest.py b/api/tests/federation/conftest.py
deleted file mode 100644
index c5831914bef6a59ddc80e88731c29127ec7b38b3..0000000000000000000000000000000000000000
--- a/api/tests/federation/conftest.py
+++ /dev/null
@@ -1,10 +0,0 @@
-import pytest
-
-
-@pytest.fixture
-def authenticated_actor(nodb_factories, mocker):
-    actor = nodb_factories['federation.Actor']()
-    mocker.patch(
-        'funkwhale_api.federation.authentication.SignatureAuthentication.authenticate_actor',
-        return_value=actor)
-    yield actor
diff --git a/api/tests/federation/test_activity.py b/api/tests/federation/test_activity.py
index a6e1d28aa23623251a1ab26661d2814c14704f00..c2673ff3bd1ff18f7cb1f2486c2e18516e36d86d 100644
--- a/api/tests/federation/test_activity.py
+++ b/api/tests/federation/test_activity.py
@@ -1,11 +1,16 @@
+import uuid
+
 from funkwhale_api.federation import activity
+from funkwhale_api.federation import serializers
+
 
-def test_deliver(nodb_factories, r_mock, mocker):
-    to = nodb_factories['federation.Actor']()
+def test_deliver(factories, r_mock, mocker, settings):
+    settings.CELERY_TASK_ALWAYS_EAGER = True
+    to = factories['federation.Actor']()
     mocker.patch(
         'funkwhale_api.federation.actors.get_actor',
         return_value=to)
-    sender = nodb_factories['federation.Actor']()
+    sender = factories['federation.Actor']()
     ac = {
         'id': 'http://test.federation/activity',
         'type': 'Create',
@@ -30,3 +35,14 @@ def test_deliver(nodb_factories, r_mock, mocker):
     assert r_mock.call_count == 1
     assert request.url == to.inbox_url
     assert request.headers['content-type'] == 'application/activity+json'
+
+
+def test_accept_follow(mocker, factories):
+    deliver = mocker.patch(
+        'funkwhale_api.federation.activity.deliver')
+    follow = factories['federation.Follow'](approved=None)
+    expected_accept = serializers.AcceptFollowSerializer(follow).data
+    activity.accept_follow(follow)
+    deliver.assert_called_once_with(
+        expected_accept, to=[follow.actor.url], on_behalf_of=follow.target
+    )
diff --git a/api/tests/federation/test_actors.py b/api/tests/federation/test_actors.py
index b3b0f8df0bb7fc3b8295bb48b6b8bd6ad967c01f..7281147a1b143a9d48adce68bf124b8a57009933 100644
--- a/api/tests/federation/test_actors.py
+++ b/api/tests/federation/test_actors.py
@@ -1,13 +1,19 @@
+import arrow
 import pytest
+import uuid
 
 from django.urls import reverse
 from django.utils import timezone
 
 from rest_framework import exceptions
 
+from funkwhale_api.federation import activity
 from funkwhale_api.federation import actors
+from funkwhale_api.federation import models
 from funkwhale_api.federation import serializers
 from funkwhale_api.federation import utils
+from funkwhale_api.music import models as music_models
+from funkwhale_api.music import tasks as music_tasks
 
 
 def test_actor_fetching(r_mock):
@@ -23,14 +29,17 @@ def test_actor_fetching(r_mock):
     assert r == payload
 
 
-def test_get_library(settings, preferences):
-    preferences['federation__public_key'] = 'public_key'
+def test_get_library(db, settings, mocker):
+    get_key_pair = mocker.patch(
+        'funkwhale_api.federation.keys.get_key_pair',
+        return_value=(b'private', b'public'))
     expected = {
         'preferred_username': 'library',
         'domain': settings.FEDERATION_HOSTNAME,
         'type': 'Person',
         'name': '{}\'s library'.format(settings.FEDERATION_HOSTNAME),
         'manually_approves_followers': True,
+        'public_key': 'public',
         'url': utils.full_url(
             reverse(
                 'federation:instance-actors-detail',
@@ -47,7 +56,6 @@ def test_get_library(settings, preferences):
             reverse(
                 'federation:instance-actors-outbox',
                 kwargs={'actor': 'library'})),
-        'public_key': 'public_key',
         'summary': 'Bot account to federate with {}\'s library'.format(
         settings.FEDERATION_HOSTNAME),
     }
@@ -56,14 +64,17 @@ def test_get_library(settings, preferences):
         assert getattr(actor, key) == value
 
 
-def test_get_test(settings, preferences):
-    preferences['federation__public_key'] = 'public_key'
+def test_get_test(db, mocker, settings):
+    get_key_pair = mocker.patch(
+        'funkwhale_api.federation.keys.get_key_pair',
+        return_value=(b'private', b'public'))
     expected = {
         'preferred_username': 'test',
         'domain': settings.FEDERATION_HOSTNAME,
         'type': 'Person',
         'name': '{}\'s test account'.format(settings.FEDERATION_HOSTNAME),
         'manually_approves_followers': False,
+        'public_key': 'public',
         'url': utils.full_url(
             reverse(
                 'federation:instance-actors-detail',
@@ -80,7 +91,6 @@ def test_get_test(settings, preferences):
             reverse(
                 'federation:instance-actors-outbox',
                 kwargs={'actor': 'test'})),
-        'public_key': 'public_key',
         'summary': 'Bot account to test federation with {}. Send me /ping and I\'ll answer you.'.format(
         settings.FEDERATION_HOSTNAME),
     }
@@ -91,18 +101,18 @@ def test_get_test(settings, preferences):
 
 def test_test_get_outbox():
     expected = {
-    	"@context": [
-    		"https://www.w3.org/ns/activitystreams",
-    		"https://w3id.org/security/v1",
-    		{}
-    	],
-    	"id": utils.full_url(
+        "@context": [
+            "https://www.w3.org/ns/activitystreams",
+            "https://w3id.org/security/v1",
+            {}
+        ],
+        "id": utils.full_url(
             reverse(
                 'federation:instance-actors-outbox',
                 kwargs={'actor': 'test'})),
-    	"type": "OrderedCollection",
-    	"totalItems": 0,
-    	"orderedItems": []
+        "type": "OrderedCollection",
+        "totalItems": 0,
+        "orderedItems": []
     }
 
     data = actors.SYSTEM_ACTORS['test'].get_outbox({}, actor=None)
@@ -126,7 +136,7 @@ def test_test_post_outbox_validates_actor(nodb_factories):
         assert msg in exc_info.value
 
 
-def test_test_post_outbox_handles_create_note(
+def test_test_post_inbox_handles_create_note(
         settings, mocker, factories):
     deliver = mocker.patch(
         'funkwhale_api.federation.activity.deliver')
@@ -167,11 +177,7 @@ def test_test_post_outbox_handles_create_note(
         }]
     )
     expected_activity = {
-        '@context': [
-            'https://www.w3.org/ns/activitystreams',
-            'https://w3id.org/security/v1',
-            {}
-        ],
+        '@context': serializers.AP_CONTEXT,
         'actor': test_actor.url,
         'id': 'https://{}/activities/note/{}/activity'.format(
             settings.FEDERATION_HOSTNAME, now.timestamp()
@@ -188,3 +194,335 @@ def test_test_post_outbox_handles_create_note(
         to=[actor.url],
         on_behalf_of=actors.SYSTEM_ACTORS['test'].get_actor_instance()
     )
+
+
+def test_getting_actor_instance_persists_in_db(db):
+    test = actors.SYSTEM_ACTORS['test'].get_actor_instance()
+    from_db = models.Actor.objects.get(url=test.url)
+
+    for f in test._meta.fields:
+        assert getattr(from_db, f.name) == getattr(test, f.name)
+
+
+@pytest.mark.parametrize('username,domain,expected', [
+    ('test', 'wrongdomain.com', False),
+    ('notsystem', '', False),
+    ('test', '', True),
+])
+def test_actor_is_system(
+        username, domain, expected, nodb_factories, settings):
+    if not domain:
+        domain = settings.FEDERATION_HOSTNAME
+
+    actor = nodb_factories['federation.Actor'](
+        preferred_username=username,
+        domain=domain,
+    )
+    assert actor.is_system is expected
+
+
+@pytest.mark.parametrize('username,domain,expected', [
+    ('test', 'wrongdomain.com', None),
+    ('notsystem', '', None),
+    ('test', '', actors.SYSTEM_ACTORS['test']),
+])
+def test_actor_is_system(
+        username, domain, expected, nodb_factories, settings):
+    if not domain:
+        domain = settings.FEDERATION_HOSTNAME
+    actor = nodb_factories['federation.Actor'](
+        preferred_username=username,
+        domain=domain,
+    )
+    assert actor.system_conf == expected
+
+
+@pytest.mark.parametrize('value', [False, True])
+def test_library_actor_manually_approves_based_on_setting(
+        value, settings):
+    settings.FEDERATION_MUSIC_NEEDS_APPROVAL = value
+    library_conf = actors.SYSTEM_ACTORS['library']
+    assert library_conf.manually_approves_followers is value
+
+
+def test_system_actor_handle(mocker, nodb_factories):
+    handler = mocker.patch(
+        'funkwhale_api.federation.actors.TestActor.handle_create')
+    actor = nodb_factories['federation.Actor']()
+    activity = nodb_factories['federation.Activity'](
+        type='Create', actor=actor.url)
+    serializer = serializers.ActivitySerializer(
+        data=activity
+    )
+    assert serializer.is_valid()
+    actors.SYSTEM_ACTORS['test'].handle(activity, actor)
+    handler.assert_called_once_with(activity, actor)
+
+
+def test_test_actor_handles_follow(
+        settings, mocker, factories):
+    deliver = mocker.patch(
+        'funkwhale_api.federation.activity.deliver')
+    actor = factories['federation.Actor']()
+    accept_follow = mocker.patch(
+        'funkwhale_api.federation.activity.accept_follow')
+    test_actor = actors.SYSTEM_ACTORS['test'].get_actor_instance()
+    data = {
+        'actor': actor.url,
+        'type': 'Follow',
+        'id': 'http://test.federation/user#follows/267',
+        'object': test_actor.url,
+    }
+    actors.SYSTEM_ACTORS['test'].post_inbox(data, actor=actor)
+    follow = models.Follow.objects.get(target=test_actor, approved=True)
+    follow_back = models.Follow.objects.get(actor=test_actor, approved=None)
+    accept_follow.assert_called_once_with(follow)
+    deliver.assert_called_once_with(
+        serializers.FollowSerializer(follow_back).data,
+        on_behalf_of=test_actor,
+        to=[actor.url]
+    )
+
+
+def test_test_actor_handles_undo_follow(
+        settings, mocker, factories):
+    deliver = mocker.patch(
+        'funkwhale_api.federation.activity.deliver')
+    test_actor = actors.SYSTEM_ACTORS['test'].get_actor_instance()
+    follow = factories['federation.Follow'](target=test_actor)
+    reverse_follow = factories['federation.Follow'](
+        actor=test_actor, target=follow.actor)
+    follow_serializer = serializers.FollowSerializer(follow)
+    reverse_follow_serializer = serializers.FollowSerializer(
+        reverse_follow)
+    undo = {
+        '@context': serializers.AP_CONTEXT,
+        'type': 'Undo',
+        'id': follow_serializer.data['id'] + '/undo',
+        'actor': follow.actor.url,
+        'object': follow_serializer.data,
+    }
+    expected_undo = {
+        '@context': serializers.AP_CONTEXT,
+        'type': 'Undo',
+        'id': reverse_follow_serializer.data['id'] + '/undo',
+        'actor': reverse_follow.actor.url,
+        'object': reverse_follow_serializer.data,
+    }
+
+    actors.SYSTEM_ACTORS['test'].post_inbox(undo, actor=follow.actor)
+    deliver.assert_called_once_with(
+        expected_undo,
+        to=[follow.actor.url],
+        on_behalf_of=test_actor,)
+
+    assert models.Follow.objects.count() == 0
+
+
+def test_library_actor_handles_follow_manual_approval(
+        settings, mocker, factories):
+    settings.FEDERATION_MUSIC_NEEDS_APPROVAL = True
+    actor = factories['federation.Actor']()
+    now = timezone.now()
+    mocker.patch('django.utils.timezone.now', return_value=now)
+    library_actor = actors.SYSTEM_ACTORS['library'].get_actor_instance()
+    data = {
+        'actor': actor.url,
+        'type': 'Follow',
+        'id': 'http://test.federation/user#follows/267',
+        'object': library_actor.url,
+    }
+
+    library_actor.system_conf.post_inbox(data, actor=actor)
+    follow = library_actor.received_follows.first()
+
+    assert follow.actor == actor
+    assert follow.approved is None
+
+
+def test_library_actor_handles_follow_auto_approval(
+        settings, mocker, factories):
+    settings.FEDERATION_MUSIC_NEEDS_APPROVAL = False
+    actor = factories['federation.Actor']()
+    accept_follow = mocker.patch(
+        'funkwhale_api.federation.activity.accept_follow')
+    library_actor = actors.SYSTEM_ACTORS['library'].get_actor_instance()
+    data = {
+        'actor': actor.url,
+        'type': 'Follow',
+        'id': 'http://test.federation/user#follows/267',
+        'object': library_actor.url,
+    }
+    library_actor.system_conf.post_inbox(data, actor=actor)
+
+    follow = library_actor.received_follows.first()
+
+    assert follow.actor == actor
+    assert follow.approved is True
+
+
+def test_library_actor_handles_accept(
+        mocker, factories):
+    library_actor = actors.SYSTEM_ACTORS['library'].get_actor_instance()
+    actor = factories['federation.Actor']()
+    pending_follow = factories['federation.Follow'](
+        actor=library_actor,
+        target=actor,
+        approved=None,
+    )
+    serializer = serializers.AcceptFollowSerializer(pending_follow)
+    library_actor.system_conf.post_inbox(serializer.data, actor=actor)
+
+    pending_follow.refresh_from_db()
+
+    assert pending_follow.approved is True
+
+
+def test_library_actor_handle_create_audio_no_library(mocker, factories):
+    # when we receive inbox create audio, we should not do anything
+    # if we don't have a configured library matching the sender
+    mocked_create = mocker.patch(
+        'funkwhale_api.federation.serializers.AudioSerializer.create'
+    )
+    actor = factories['federation.Actor']()
+    library_actor = actors.SYSTEM_ACTORS['library'].get_actor_instance()
+    data = {
+        'actor': actor.url,
+        'type': 'Create',
+        'id': 'http://test.federation/audio/create',
+        'object': {
+            'id': 'https://batch.import',
+            'type': 'Collection',
+            'totalItems': 2,
+            'items': factories['federation.Audio'].create_batch(size=2)
+        },
+    }
+    library_actor.system_conf.post_inbox(data, actor=actor)
+
+    mocked_create.assert_not_called()
+    models.LibraryTrack.objects.count() == 0
+
+
+def test_library_actor_handle_create_audio_no_library_enabled(
+        mocker, factories):
+    # when we receive inbox create audio, we should not do anything
+    # if we don't have an enabled library
+    mocked_create = mocker.patch(
+        'funkwhale_api.federation.serializers.AudioSerializer.create'
+    )
+    disabled_library = factories['federation.Library'](
+        federation_enabled=False)
+    actor = disabled_library.actor
+    library_actor = actors.SYSTEM_ACTORS['library'].get_actor_instance()
+    data = {
+        'actor': actor.url,
+        'type': 'Create',
+        'id': 'http://test.federation/audio/create',
+        'object': {
+            'id': 'https://batch.import',
+            'type': 'Collection',
+            'totalItems': 2,
+            'items': factories['federation.Audio'].create_batch(size=2)
+        },
+    }
+    library_actor.system_conf.post_inbox(data, actor=actor)
+
+    mocked_create.assert_not_called()
+    models.LibraryTrack.objects.count() == 0
+
+
+def test_library_actor_handle_create_audio(mocker, factories):
+    library_actor = actors.SYSTEM_ACTORS['library'].get_actor_instance()
+    remote_library = factories['federation.Library'](
+        federation_enabled=True
+    )
+
+    data = {
+        'actor': remote_library.actor.url,
+        'type': 'Create',
+        'id': 'http://test.federation/audio/create',
+        'object': {
+            'id': 'https://batch.import',
+            'type': 'Collection',
+            'totalItems': 2,
+            'items': factories['federation.Audio'].create_batch(size=2)
+        },
+    }
+
+    library_actor.system_conf.post_inbox(data, actor=remote_library.actor)
+
+    lts = list(remote_library.tracks.order_by('id'))
+
+    assert len(lts) == 2
+
+    for i, a in enumerate(data['object']['items']):
+        lt = lts[i]
+        assert lt.pk is not None
+        assert lt.url == a['id']
+        assert lt.library == remote_library
+        assert lt.audio_url == a['url']['href']
+        assert lt.audio_mimetype == a['url']['mediaType']
+        assert lt.metadata == a['metadata']
+        assert lt.title == a['metadata']['recording']['title']
+        assert lt.artist_name == a['metadata']['artist']['name']
+        assert lt.album_title == a['metadata']['release']['title']
+        assert lt.published_date == arrow.get(a['published'])
+
+
+def test_library_actor_handle_create_audio_autoimport(mocker, factories):
+    mocked_import = mocker.patch(
+        'funkwhale_api.common.utils.on_commit')
+    library_actor = actors.SYSTEM_ACTORS['library'].get_actor_instance()
+    remote_library = factories['federation.Library'](
+        federation_enabled=True,
+        autoimport=True,
+    )
+
+    data = {
+        'actor': remote_library.actor.url,
+        'type': 'Create',
+        'id': 'http://test.federation/audio/create',
+        'object': {
+            'id': 'https://batch.import',
+            'type': 'Collection',
+            'totalItems': 2,
+            'items': factories['federation.Audio'].create_batch(size=2)
+        },
+    }
+
+    library_actor.system_conf.post_inbox(data, actor=remote_library.actor)
+
+    lts = list(remote_library.tracks.order_by('id'))
+
+    assert len(lts) == 2
+
+    for i, a in enumerate(data['object']['items']):
+        lt = lts[i]
+        assert lt.pk is not None
+        assert lt.url == a['id']
+        assert lt.library == remote_library
+        assert lt.audio_url == a['url']['href']
+        assert lt.audio_mimetype == a['url']['mediaType']
+        assert lt.metadata == a['metadata']
+        assert lt.title == a['metadata']['recording']['title']
+        assert lt.artist_name == a['metadata']['artist']['name']
+        assert lt.album_title == a['metadata']['release']['title']
+        assert lt.published_date == arrow.get(a['published'])
+
+    batch = music_models.ImportBatch.objects.latest('id')
+
+    assert batch.jobs.count() == len(lts)
+    assert batch.source == 'federation'
+    assert batch.submitted_by is None
+
+    for i, job in enumerate(batch.jobs.order_by('id')):
+        lt = lts[i]
+        assert job.library_track == lt
+        assert job.mbid == lt.mbid
+        assert job.source == lt.url
+
+        mocked_import.assert_any_call(
+            music_tasks.import_job_run.delay,
+            import_job_id=job.pk,
+            use_acoustid=False,
+        )
diff --git a/api/tests/federation/test_authentication.py b/api/tests/federation/test_authentication.py
index 1837b3950f471ba549d8f45077d5b773cc010da3..2f69e4d4f1dcd2718e7148ebb9887c22e2def3df 100644
--- a/api/tests/federation/test_authentication.py
+++ b/api/tests/federation/test_authentication.py
@@ -3,22 +3,24 @@ from funkwhale_api.federation import keys
 from funkwhale_api.federation import signing
 
 
-def test_authenticate(nodb_factories, mocker, api_request):
+def test_authenticate(factories, mocker, api_request):
     private, public = keys.get_key_pair()
     actor_url = 'https://test.federation/actor'
     mocker.patch(
         'funkwhale_api.federation.actors.get_actor_data',
         return_value={
             'id': actor_url,
+            'type': 'Person',
             'outbox': 'https://test.com',
             'inbox': 'https://test.com',
+            'preferredUsername': 'test',
             'publicKey': {
                 'publicKeyPem': public.decode('utf-8'),
                 'owner': actor_url,
                 'id': actor_url + '#main-key',
             }
         })
-    signed_request = nodb_factories['federation.SignedRequest'](
+    signed_request = factories['federation.SignedRequest'](
         auth__key=private,
         auth__key_id=actor_url + '#main-key',
         auth__headers=[
diff --git a/api/tests/federation/test_commands.py b/api/tests/federation/test_commands.py
deleted file mode 100644
index 7c533306821a24a664c260089c0f15201c5a9870..0000000000000000000000000000000000000000
--- a/api/tests/federation/test_commands.py
+++ /dev/null
@@ -1,14 +0,0 @@
-from django.core.management import call_command
-
-
-def test_generate_instance_key_pair(preferences, mocker):
-    mocker.patch(
-        'funkwhale_api.federation.keys.get_key_pair',
-        return_value=(b'private', b'public'))
-    assert preferences['federation__public_key'] == ''
-    assert preferences['federation__private_key'] == ''
-
-    call_command('generate_keys', interactive=False)
-
-    assert preferences['federation__private_key'] == 'private'
-    assert preferences['federation__public_key'] == 'public'
diff --git a/api/tests/federation/test_library.py b/api/tests/federation/test_library.py
new file mode 100644
index 0000000000000000000000000000000000000000..7a3abf5d8375ee63598998b40a3b8977b2942198
--- /dev/null
+++ b/api/tests/federation/test_library.py
@@ -0,0 +1,70 @@
+from funkwhale_api.federation import library
+from funkwhale_api.federation import serializers
+
+
+def test_library_scan_from_account_name(mocker, factories):
+    actor = factories['federation.Actor'](
+        preferred_username='library',
+        domain='test.library'
+    )
+    get_resource_result = {'actor_url': actor.url}
+    get_resource = mocker.patch(
+        'funkwhale_api.federation.webfinger.get_resource',
+        return_value=get_resource_result)
+
+    actor_data = serializers.ActorSerializer(actor).data
+    actor_data['manuallyApprovesFollowers'] = False
+    actor_data['url'] = [{
+        'type': 'Link',
+        'name': 'library',
+        'mediaType': 'application/activity+json',
+        'href': 'https://test.library'
+    }]
+    get_actor_data = mocker.patch(
+        'funkwhale_api.federation.actors.get_actor_data',
+        return_value=actor_data)
+
+    get_library_data_result = {'test': 'test'}
+    get_library_data = mocker.patch(
+        'funkwhale_api.federation.library.get_library_data',
+        return_value=get_library_data_result)
+
+    result = library.scan_from_account_name('library@test.actor')
+
+    get_resource.assert_called_once_with('acct:library@test.actor')
+    get_actor_data.assert_called_once_with(actor.url)
+    get_library_data.assert_called_once_with(actor_data['url'][0]['href'])
+
+    assert result == {
+        'webfinger': get_resource_result,
+        'actor': actor_data,
+        'library': get_library_data_result,
+        'local': {
+            'following': False,
+            'awaiting_approval': False,
+        },
+    }
+
+
+def test_get_library_data(r_mock, factories):
+    actor = factories['federation.Actor']()
+    url = 'https://test.library'
+    conf = {
+        'id': url,
+        'items': [],
+        'actor': actor,
+        'page_size': 5,
+    }
+    data = serializers.PaginatedCollectionSerializer(conf).data
+    r_mock.get(url, json=data)
+
+    result = library.get_library_data(url)
+    for f in ['totalItems', 'actor', 'id', 'type']:
+        assert result[f] == data[f]
+
+
+def test_get_library_data_requires_authentication(r_mock, factories):
+    url = 'https://test.library'
+    r_mock.get(url, status_code=403)
+    result = library.get_library_data(url)
+    assert result['errors'] == ['Permission denied while scanning library']
diff --git a/api/tests/federation/test_models.py b/api/tests/federation/test_models.py
new file mode 100644
index 0000000000000000000000000000000000000000..ae158e659fc105bf18a96264cc6968fdab387719
--- /dev/null
+++ b/api/tests/federation/test_models.py
@@ -0,0 +1,41 @@
+import pytest
+import uuid
+
+from django import db
+
+from funkwhale_api.federation import models
+from funkwhale_api.federation import serializers
+
+
+def test_cannot_duplicate_actor(factories):
+    actor = factories['federation.Actor']()
+
+    with pytest.raises(db.IntegrityError):
+        factories['federation.Actor'](
+            domain=actor.domain,
+            preferred_username=actor.preferred_username,
+        )
+
+
+def test_cannot_duplicate_follow(factories):
+    follow = factories['federation.Follow']()
+
+    with pytest.raises(db.IntegrityError):
+        factories['federation.Follow'](
+            target=follow.target,
+            actor=follow.actor,
+        )
+
+
+def test_follow_federation_url(factories):
+    follow = factories['federation.Follow'](local=True)
+    expected = '{}#follows/{}'.format(
+        follow.actor.url, follow.uuid)
+
+    assert follow.get_federation_url() == expected
+
+
+def test_library_model_unique_per_actor(factories):
+    library = factories['federation.Library']()
+    with pytest.raises(db.IntegrityError):
+        factories['federation.Library'](actor=library.actor)
diff --git a/api/tests/federation/test_permissions.py b/api/tests/federation/test_permissions.py
new file mode 100644
index 0000000000000000000000000000000000000000..1a69775422db94298925e5ff839a79117201c6dd
--- /dev/null
+++ b/api/tests/federation/test_permissions.py
@@ -0,0 +1,45 @@
+from rest_framework.views import APIView
+
+from funkwhale_api.federation import actors
+from funkwhale_api.federation import permissions
+
+
+def test_library_follower(
+        factories, api_request, anonymous_user, settings):
+    settings.FEDERATION_MUSIC_NEEDS_APPROVAL = True
+    view = APIView.as_view()
+    permission = permissions.LibraryFollower()
+    request = api_request.get('/')
+    setattr(request, 'user', anonymous_user)
+    check = permission.has_permission(request, view)
+
+    assert check is False
+
+
+def test_library_follower_actor_non_follower(
+        factories, api_request, anonymous_user, settings):
+    settings.FEDERATION_MUSIC_NEEDS_APPROVAL = True
+    actor = factories['federation.Actor']()
+    view = APIView.as_view()
+    permission = permissions.LibraryFollower()
+    request = api_request.get('/')
+    setattr(request, 'user', anonymous_user)
+    setattr(request, 'actor', actor)
+    check = permission.has_permission(request, view)
+
+    assert check is False
+
+
+def test_library_follower_actor_follower(
+        factories, api_request, anonymous_user, settings):
+    settings.FEDERATION_MUSIC_NEEDS_APPROVAL = True
+    library = actors.SYSTEM_ACTORS['library'].get_actor_instance()
+    follow = factories['federation.Follow'](target=library)
+    view = APIView.as_view()
+    permission = permissions.LibraryFollower()
+    request = api_request.get('/')
+    setattr(request, 'user', anonymous_user)
+    setattr(request, 'actor', follow.actor)
+    check = permission.has_permission(request, view)
+
+    assert check is True
diff --git a/api/tests/federation/test_serializers.py b/api/tests/federation/test_serializers.py
index efa92b16a26dcdf72287282895872c10e5d10070..6d33a529de7d197d0b88557cc38ee8c17ff46ce4 100644
--- a/api/tests/federation/test_serializers.py
+++ b/api/tests/federation/test_serializers.py
@@ -1,35 +1,41 @@
+import arrow
+import pytest
+
 from django.urls import reverse
+from django.core.paginator import Paginator
 
+from funkwhale_api.federation import actors
 from funkwhale_api.federation import keys
 from funkwhale_api.federation import models
 from funkwhale_api.federation import serializers
+from funkwhale_api.federation import utils
 
 
 def test_actor_serializer_from_ap(db):
     payload = {
-    	'id': 'https://test.federation/user',
-    	'type': 'Person',
-    	'following': 'https://test.federation/user/following',
-    	'followers': 'https://test.federation/user/followers',
-    	'inbox': 'https://test.federation/user/inbox',
-    	'outbox': 'https://test.federation/user/outbox',
-    	'preferredUsername': 'user',
-    	'name': 'Real User',
-    	'summary': 'Hello world',
-    	'url': 'https://test.federation/@user',
-    	'manuallyApprovesFollowers': False,
-    	'publicKey': {
-    		'id': 'https://test.federation/user#main-key',
-    		'owner': 'https://test.federation/user',
-    		'publicKeyPem': 'yolo'
-    	},
-    	'endpoints': {
-    		'sharedInbox': 'https://test.federation/inbox'
-    	},
+        'id': 'https://test.federation/user',
+        'type': 'Person',
+        'following': 'https://test.federation/user/following',
+        'followers': 'https://test.federation/user/followers',
+        'inbox': 'https://test.federation/user/inbox',
+        'outbox': 'https://test.federation/user/outbox',
+        'preferredUsername': 'user',
+        'name': 'Real User',
+        'summary': 'Hello world',
+        'url': 'https://test.federation/@user',
+        'manuallyApprovesFollowers': False,
+        'publicKey': {
+            'id': 'https://test.federation/user#main-key',
+            'owner': 'https://test.federation/user',
+            'publicKeyPem': 'yolo'
+        },
+        'endpoints': {
+            'sharedInbox': 'https://test.federation/inbox'
+        },
     }
 
     serializer = serializers.ActorSerializer(data=payload)
-    assert serializer.is_valid()
+    assert serializer.is_valid(raise_exception=True)
 
     actor = serializer.build()
 
@@ -50,17 +56,17 @@ def test_actor_serializer_from_ap(db):
 
 def test_actor_serializer_only_mandatory_field_from_ap(db):
     payload = {
-    	'id': 'https://test.federation/user',
-    	'type': 'Person',
-    	'following': 'https://test.federation/user/following',
-    	'followers': 'https://test.federation/user/followers',
-    	'inbox': 'https://test.federation/user/inbox',
-    	'outbox': 'https://test.federation/user/outbox',
-    	'preferredUsername': 'user',
+        'id': 'https://test.federation/user',
+        'type': 'Person',
+        'following': 'https://test.federation/user/following',
+        'followers': 'https://test.federation/user/followers',
+        'inbox': 'https://test.federation/user/inbox',
+        'outbox': 'https://test.federation/user/outbox',
+        'preferredUsername': 'user',
     }
 
     serializer = serializers.ActorSerializer(data=payload)
-    assert serializer.is_valid()
+    assert serializer.is_valid(raise_exception=True)
 
     actor = serializer.build()
 
@@ -82,24 +88,24 @@ def test_actor_serializer_to_ap():
             'https://w3id.org/security/v1',
             {},
         ],
-    	'id': 'https://test.federation/user',
-    	'type': 'Person',
-    	'following': 'https://test.federation/user/following',
-    	'followers': 'https://test.federation/user/followers',
-    	'inbox': 'https://test.federation/user/inbox',
-    	'outbox': 'https://test.federation/user/outbox',
-    	'preferredUsername': 'user',
-    	'name': 'Real User',
-    	'summary': 'Hello world',
-    	'manuallyApprovesFollowers': False,
-    	'publicKey': {
-    		'id': 'https://test.federation/user#main-key',
-    		'owner': 'https://test.federation/user',
-    		'publicKeyPem': 'yolo'
-    	},
-    	'endpoints': {
-    		'sharedInbox': 'https://test.federation/inbox'
-    	},
+        'id': 'https://test.federation/user',
+        'type': 'Person',
+        'following': 'https://test.federation/user/following',
+        'followers': 'https://test.federation/user/followers',
+        'inbox': 'https://test.federation/user/inbox',
+        'outbox': 'https://test.federation/user/outbox',
+        'preferredUsername': 'user',
+        'name': 'Real User',
+        'summary': 'Hello world',
+        'manuallyApprovesFollowers': False,
+        'publicKey': {
+            'id': 'https://test.federation/user#main-key',
+            'owner': 'https://test.federation/user',
+            'publicKeyPem': 'yolo'
+        },
+        'endpoints': {
+            'sharedInbox': 'https://test.federation/inbox'
+        },
     }
     ac = models.Actor(
         url=expected['id'],
@@ -144,3 +150,534 @@ def test_webfinger_serializer():
     serializer = serializers.ActorWebfingerSerializer(actor)
 
     assert serializer.data == expected
+
+
+def test_follow_serializer_to_ap(factories):
+    follow = factories['federation.Follow'](local=True)
+    serializer = serializers.FollowSerializer(follow)
+
+    expected = {
+        '@context': [
+            'https://www.w3.org/ns/activitystreams',
+            'https://w3id.org/security/v1',
+            {},
+        ],
+        'id': follow.get_federation_url(),
+        'type': 'Follow',
+        'actor': follow.actor.url,
+        'object': follow.target.url,
+    }
+
+    assert serializer.data == expected
+
+
+def test_follow_serializer_save(factories):
+    actor = factories['federation.Actor']()
+    target = factories['federation.Actor']()
+
+    data = expected = {
+        'id': 'https://test.follow',
+        'type': 'Follow',
+        'actor': actor.url,
+        'object': target.url,
+    }
+    serializer = serializers.FollowSerializer(data=data)
+
+    assert serializer.is_valid(raise_exception=True)
+
+    follow = serializer.save()
+
+    assert follow.pk is not None
+    assert follow.actor == actor
+    assert follow.target == target
+    assert follow.approved is None
+
+
+def test_follow_serializer_save_validates_on_context(factories):
+    actor = factories['federation.Actor']()
+    target = factories['federation.Actor']()
+    impostor = factories['federation.Actor']()
+
+    data = expected = {
+        'id': 'https://test.follow',
+        'type': 'Follow',
+        'actor': actor.url,
+        'object': target.url,
+    }
+    serializer = serializers.FollowSerializer(
+        data=data,
+        context={'follow_actor': impostor, 'follow_target': impostor})
+
+    assert serializer.is_valid() is False
+
+    assert 'actor' in serializer.errors
+    assert 'object' in serializer.errors
+
+
+def test_accept_follow_serializer_representation(factories):
+    follow = factories['federation.Follow'](approved=None)
+
+    expected = {
+        '@context': [
+            'https://www.w3.org/ns/activitystreams',
+            'https://w3id.org/security/v1',
+            {},
+        ],
+        'id': follow.get_federation_url() + '/accept',
+        'type': 'Accept',
+        'actor': follow.target.url,
+        'object': serializers.FollowSerializer(follow).data,
+    }
+
+    serializer = serializers.AcceptFollowSerializer(follow)
+
+    assert serializer.data == expected
+
+
+def test_accept_follow_serializer_save(factories):
+    follow = factories['federation.Follow'](approved=None)
+
+    data = {
+        '@context': [
+            'https://www.w3.org/ns/activitystreams',
+            'https://w3id.org/security/v1',
+            {},
+        ],
+        'id': follow.get_federation_url() + '/accept',
+        'type': 'Accept',
+        'actor': follow.target.url,
+        'object': serializers.FollowSerializer(follow).data,
+    }
+
+    serializer = serializers.AcceptFollowSerializer(data=data)
+    assert serializer.is_valid(raise_exception=True)
+    serializer.save()
+
+    follow.refresh_from_db()
+
+    assert follow.approved is True
+
+
+def test_accept_follow_serializer_validates_on_context(factories):
+    follow = factories['federation.Follow'](approved=None)
+    impostor = factories['federation.Actor']()
+    data = {
+        '@context': [
+            'https://www.w3.org/ns/activitystreams',
+            'https://w3id.org/security/v1',
+            {},
+        ],
+        'id': follow.get_federation_url() + '/accept',
+        'type': 'Accept',
+        'actor': impostor.url,
+        'object': serializers.FollowSerializer(follow).data,
+    }
+
+    serializer = serializers.AcceptFollowSerializer(
+        data=data,
+        context={'follow_actor': impostor, 'follow_target': impostor})
+
+    assert serializer.is_valid() is False
+    assert 'actor' in serializer.errors['object']
+    assert 'object' in serializer.errors['object']
+
+
+def test_undo_follow_serializer_representation(factories):
+    follow = factories['federation.Follow'](approved=True)
+
+    expected = {
+        '@context': [
+            'https://www.w3.org/ns/activitystreams',
+            'https://w3id.org/security/v1',
+            {},
+        ],
+        'id': follow.get_federation_url() + '/undo',
+        'type': 'Undo',
+        'actor': follow.actor.url,
+        'object': serializers.FollowSerializer(follow).data,
+    }
+
+    serializer = serializers.UndoFollowSerializer(follow)
+
+    assert serializer.data == expected
+
+
+def test_undo_follow_serializer_save(factories):
+    follow = factories['federation.Follow'](approved=True)
+
+    data = {
+        '@context': [
+            'https://www.w3.org/ns/activitystreams',
+            'https://w3id.org/security/v1',
+            {},
+        ],
+        'id': follow.get_federation_url() + '/undo',
+        'type': 'Undo',
+        'actor': follow.actor.url,
+        'object': serializers.FollowSerializer(follow).data,
+    }
+
+    serializer = serializers.UndoFollowSerializer(data=data)
+    assert serializer.is_valid(raise_exception=True)
+    serializer.save()
+
+    with pytest.raises(models.Follow.DoesNotExist):
+        follow.refresh_from_db()
+
+
+def test_undo_follow_serializer_validates_on_context(factories):
+    follow = factories['federation.Follow'](approved=True)
+    impostor = factories['federation.Actor']()
+    data = {
+        '@context': [
+            'https://www.w3.org/ns/activitystreams',
+            'https://w3id.org/security/v1',
+            {},
+        ],
+        'id': follow.get_federation_url() + '/undo',
+        'type': 'Undo',
+        'actor': impostor.url,
+        'object': serializers.FollowSerializer(follow).data,
+    }
+
+    serializer = serializers.UndoFollowSerializer(
+        data=data,
+        context={'follow_actor': impostor, 'follow_target': impostor})
+
+    assert serializer.is_valid() is False
+    assert 'actor' in serializer.errors['object']
+    assert 'object' in serializer.errors['object']
+
+
+def test_paginated_collection_serializer(factories):
+    tfs = factories['music.TrackFile'].create_batch(size=5)
+    actor = factories['federation.Actor'](local=True)
+
+    conf = {
+        'id': 'https://test.federation/test',
+        'items': tfs,
+        'item_serializer': serializers.AudioSerializer,
+        'actor': actor,
+        'page_size': 2,
+    }
+    expected = {
+        '@context': [
+            'https://www.w3.org/ns/activitystreams',
+            'https://w3id.org/security/v1',
+            {},
+        ],
+        'type': 'Collection',
+        'id': conf['id'],
+        'actor': actor.url,
+        'totalItems': len(tfs),
+        'current': conf['id'] + '?page=1',
+        'last': conf['id'] + '?page=3',
+        'first': conf['id'] + '?page=1',
+    }
+
+    serializer = serializers.PaginatedCollectionSerializer(conf)
+
+    assert serializer.data == expected
+
+
+def test_paginated_collection_serializer_validation():
+    data = {
+        'type': 'Collection',
+        'id': 'https://test.federation/test',
+        'totalItems': 5,
+        'actor': 'http://test.actor',
+        'first': 'https://test.federation/test?page=1',
+        'last': 'https://test.federation/test?page=1',
+        'items': []
+    }
+
+    serializer = serializers.PaginatedCollectionSerializer(
+        data=data
+    )
+
+    assert serializer.is_valid(raise_exception=True) is True
+    assert serializer.validated_data['totalItems'] == 5
+    assert serializer.validated_data['id'] == data['id']
+    assert serializer.validated_data['actor'] == data['actor']
+
+
+def test_collection_page_serializer_validation():
+    base = 'https://test.federation/test'
+    data = {
+        'type': 'CollectionPage',
+        'id': base + '?page=2',
+        'totalItems': 5,
+        'actor': 'https://test.actor',
+        'items': [],
+        'first': 'https://test.federation/test?page=1',
+        'last': 'https://test.federation/test?page=3',
+        'prev': base + '?page=1',
+        'next': base + '?page=3',
+        'partOf': base,
+    }
+
+    serializer = serializers.CollectionPageSerializer(
+        data=data
+    )
+
+    assert serializer.is_valid(raise_exception=True) is True
+    assert serializer.validated_data['totalItems'] == 5
+    assert serializer.validated_data['id'] == data['id']
+    assert serializer.validated_data['actor'] == data['actor']
+    assert serializer.validated_data['items'] == []
+    assert serializer.validated_data['prev'] == data['prev']
+    assert serializer.validated_data['next'] == data['next']
+    assert serializer.validated_data['partOf'] == data['partOf']
+
+
+def test_collection_page_serializer_can_validate_child():
+    base = 'https://test.federation/test'
+    data = {
+        'items': [{'in': 'valid'}],
+    }
+
+    serializer = serializers.CollectionPageSerializer(
+        data=data,
+        context={'item_serializer': serializers.AudioSerializer}
+    )
+
+    assert serializer.is_valid() is False
+    assert 'items' in serializer.errors
+
+
+def test_collection_page_serializer(factories):
+    tfs = factories['music.TrackFile'].create_batch(size=5)
+    actor = factories['federation.Actor'](local=True)
+
+    conf = {
+        'id': 'https://test.federation/test',
+        'item_serializer': serializers.AudioSerializer,
+        'actor': actor,
+        'page': Paginator(tfs, 2).page(2),
+    }
+    expected = {
+        '@context': [
+            'https://www.w3.org/ns/activitystreams',
+            'https://w3id.org/security/v1',
+            {},
+        ],
+        'type': 'CollectionPage',
+        'id': conf['id'] + '?page=2',
+        'actor': actor.url,
+        'totalItems': len(tfs),
+        'partOf': conf['id'],
+        'prev': conf['id'] + '?page=1',
+        'next': conf['id'] + '?page=3',
+        'first': conf['id'] + '?page=1',
+        'last': conf['id'] + '?page=3',
+        'items': [
+            conf['item_serializer'](
+                i,
+                context={'actor': actor, 'include_ap_context': False}
+            ).data
+            for i in conf['page'].object_list
+        ]
+    }
+
+    serializer = serializers.CollectionPageSerializer(conf)
+
+    assert serializer.data == expected
+
+
+def test_activity_pub_audio_serializer_to_library_track(factories):
+    remote_library = factories['federation.Library']()
+    audio = factories['federation.Audio']()
+    serializer = serializers.AudioSerializer(
+        data=audio, context={'library': remote_library})
+
+    assert serializer.is_valid(raise_exception=True)
+
+    lt = serializer.save()
+
+    assert lt.pk is not None
+    assert lt.url == audio['id']
+    assert lt.library == remote_library
+    assert lt.audio_url == audio['url']['href']
+    assert lt.audio_mimetype == audio['url']['mediaType']
+    assert lt.metadata == audio['metadata']
+    assert lt.title == audio['metadata']['recording']['title']
+    assert lt.artist_name == audio['metadata']['artist']['name']
+    assert lt.album_title == audio['metadata']['release']['title']
+    assert lt.published_date == arrow.get(audio['published'])
+
+
+def test_activity_pub_audio_serializer_to_library_track_no_duplicate(
+        factories):
+    remote_library = factories['federation.Library']()
+    audio = factories['federation.Audio']()
+    serializer1 = serializers.AudioSerializer(
+        data=audio, context={'library': remote_library})
+    serializer2 = serializers.AudioSerializer(
+        data=audio, context={'library': remote_library})
+
+    assert serializer1.is_valid() is True
+    assert serializer2.is_valid() is True
+
+    lt1 = serializer1.save()
+    lt2 = serializer2.save()
+
+    assert lt1 == lt2
+    assert models.LibraryTrack.objects.count() == 1
+
+
+def test_activity_pub_audio_serializer_to_ap(factories):
+    tf = factories['music.TrackFile'](mimetype='audio/mp3')
+    library = actors.SYSTEM_ACTORS['library'].get_actor_instance()
+    expected = {
+        '@context': serializers.AP_CONTEXT,
+        'type': 'Audio',
+        'id': tf.get_federation_url(),
+        'name': tf.track.full_name,
+        'published': tf.creation_date.isoformat(),
+        'updated': tf.modification_date.isoformat(),
+        'metadata': {
+            'artist': {
+                'musicbrainz_id': tf.track.artist.mbid,
+                'name': tf.track.artist.name,
+            },
+            'release': {
+                'musicbrainz_id': tf.track.album.mbid,
+                'title': tf.track.album.title,
+            },
+            'recording': {
+                'musicbrainz_id': tf.track.mbid,
+                'title': tf.track.title,
+            },
+        },
+        'url': {
+            'href': utils.full_url(tf.path),
+            'type': 'Link',
+            'mediaType': 'audio/mp3'
+        },
+        'attributedTo': [
+            library.url
+        ]
+    }
+
+    serializer = serializers.AudioSerializer(tf, context={'actor': library})
+
+    assert serializer.data == expected
+
+
+def test_activity_pub_audio_serializer_to_ap_no_mbid(factories):
+    tf = factories['music.TrackFile'](
+        mimetype='audio/mp3',
+        track__mbid=None,
+        track__album__mbid=None,
+        track__album__artist__mbid=None,
+    )
+    library = actors.SYSTEM_ACTORS['library'].get_actor_instance()
+    expected = {
+        '@context': serializers.AP_CONTEXT,
+        'type': 'Audio',
+        'id': tf.get_federation_url(),
+        'name': tf.track.full_name,
+        'published': tf.creation_date.isoformat(),
+        'updated': tf.modification_date.isoformat(),
+        'metadata': {
+            'artist': {
+                'name': tf.track.artist.name,
+                'musicbrainz_id': None,
+            },
+            'release': {
+                'title': tf.track.album.title,
+                'musicbrainz_id': None,
+            },
+            'recording': {
+                'title': tf.track.title,
+                'musicbrainz_id': None,
+            },
+        },
+        'url': {
+            'href': utils.full_url(tf.path),
+            'type': 'Link',
+            'mediaType': 'audio/mp3'
+        },
+        'attributedTo': [
+            library.url
+        ]
+    }
+
+    serializer = serializers.AudioSerializer(tf, context={'actor': library})
+
+    assert serializer.data == expected
+
+
+def test_collection_serializer_to_ap(factories):
+    tf1 = factories['music.TrackFile'](mimetype='audio/mp3')
+    tf2 = factories['music.TrackFile'](mimetype='audio/ogg')
+    library = actors.SYSTEM_ACTORS['library'].get_actor_instance()
+    expected = {
+        '@context': serializers.AP_CONTEXT,
+        'id': 'https://test.id',
+        'actor': library.url,
+        'totalItems': 2,
+        'type': 'Collection',
+        'items': [
+            serializers.AudioSerializer(
+                tf1, context={'actor': library, 'include_ap_context': False}
+            ).data,
+            serializers.AudioSerializer(
+                tf2, context={'actor': library, 'include_ap_context': False}
+            ).data,
+        ]
+    }
+
+    collection = {
+        'id': expected['id'],
+        'actor': library,
+        'items': [tf1, tf2],
+        'item_serializer': serializers.AudioSerializer
+    }
+    serializer = serializers.CollectionSerializer(
+        collection, context={'actor': library, 'id': 'https://test.id'})
+
+    assert serializer.data == expected
+
+
+def test_api_library_create_serializer_save(factories, r_mock):
+    library_actor = actors.SYSTEM_ACTORS['library'].get_actor_instance()
+    actor = factories['federation.Actor']()
+    follow = factories['federation.Follow'](
+        target=actor,
+        actor=library_actor,
+    )
+    actor_data = serializers.ActorSerializer(actor).data
+    actor_data['url'] = [{
+        'href': 'https://test.library',
+        'name': 'library',
+        'type': 'Link',
+    }]
+    library_conf = {
+        'id': 'https://test.library',
+        'items': range(10),
+        'actor': actor,
+        'page_size': 5,
+    }
+    library_data = serializers.PaginatedCollectionSerializer(library_conf).data
+    r_mock.get(actor.url, json=actor_data)
+    r_mock.get('https://test.library', json=library_data)
+    data = {
+        'actor': actor.url,
+        'autoimport': False,
+        'federation_enabled': True,
+        'download_files': False,
+    }
+
+    serializer = serializers.APILibraryCreateSerializer(data=data)
+    assert serializer.is_valid(raise_exception=True) is True
+    library = serializer.save()
+    follow = models.Follow.objects.get(
+        target=actor, actor=library_actor, approved=None)
+
+    assert library.autoimport is data['autoimport']
+    assert library.federation_enabled is data['federation_enabled']
+    assert library.download_files is data['download_files']
+    assert library.tracks_count == 10
+    assert library.actor == actor
+    assert library.follow == follow
diff --git a/api/tests/federation/test_tasks.py b/api/tests/federation/test_tasks.py
new file mode 100644
index 0000000000000000000000000000000000000000..506fbc1fe95be93320161e28f626f450f7b4000f
--- /dev/null
+++ b/api/tests/federation/test_tasks.py
@@ -0,0 +1,140 @@
+import datetime
+
+from django.core.paginator import Paginator
+from django.utils import timezone
+
+from funkwhale_api.federation import serializers
+from funkwhale_api.federation import tasks
+
+
+def test_scan_library_does_nothing_if_federation_disabled(mocker, factories):
+    library = factories['federation.Library'](federation_enabled=False)
+    tasks.scan_library(library_id=library.pk)
+
+    assert library.tracks.count() == 0
+
+
+def test_scan_library_page_does_nothing_if_federation_disabled(
+        mocker, factories):
+    library = factories['federation.Library'](federation_enabled=False)
+    tasks.scan_library_page(library_id=library.pk, page_url=None)
+
+    assert library.tracks.count() == 0
+
+
+def test_scan_library_fetches_page_and_calls_scan_page(
+        mocker, factories, r_mock):
+    now = timezone.now()
+    library = factories['federation.Library'](federation_enabled=True)
+    collection_conf = {
+        'actor': library.actor,
+        'id': library.url,
+        'page_size': 10,
+        'items': range(10),
+    }
+    collection = serializers.PaginatedCollectionSerializer(collection_conf)
+    scan_page = mocker.patch(
+        'funkwhale_api.federation.tasks.scan_library_page.delay')
+    r_mock.get(collection_conf['id'], json=collection.data)
+    tasks.scan_library(library_id=library.pk)
+
+    scan_page.assert_called_once_with(
+        library_id=library.id,
+        page_url=collection.data['first'],
+        until=None,
+    )
+    library.refresh_from_db()
+    assert library.fetched_date > now
+
+
+def test_scan_page_fetches_page_and_creates_tracks(
+        mocker, factories, r_mock):
+    library = factories['federation.Library'](federation_enabled=True)
+    tfs = factories['music.TrackFile'].create_batch(size=5)
+    page_conf = {
+        'actor': library.actor,
+        'id': library.url,
+        'page': Paginator(tfs, 5).page(1),
+        'item_serializer': serializers.AudioSerializer,
+    }
+    page = serializers.CollectionPageSerializer(page_conf)
+    r_mock.get(page.data['id'], json=page.data)
+
+    tasks.scan_library_page(library_id=library.pk, page_url=page.data['id'])
+
+    lts = list(library.tracks.all().order_by('-published_date'))
+    assert len(lts) == 5
+
+
+def test_scan_page_trigger_next_page_scan_skip_if_same(
+        mocker, factories, r_mock):
+    patched_scan = mocker.patch(
+        'funkwhale_api.federation.tasks.scan_library_page.delay'
+    )
+    library = factories['federation.Library'](federation_enabled=True)
+    tfs = factories['music.TrackFile'].create_batch(size=1)
+    page_conf = {
+        'actor': library.actor,
+        'id': library.url,
+        'page': Paginator(tfs, 3).page(1),
+        'item_serializer': serializers.AudioSerializer,
+    }
+    page = serializers.CollectionPageSerializer(page_conf)
+    data = page.data
+    data['next'] = data['id']
+    r_mock.get(page.data['id'], json=data)
+
+    tasks.scan_library_page(library_id=library.pk, page_url=data['id'])
+    patched_scan.assert_not_called()
+
+
+def test_scan_page_stops_once_until_is_reached(
+        mocker, factories, r_mock):
+    library = factories['federation.Library'](federation_enabled=True)
+    tfs = list(reversed(factories['music.TrackFile'].create_batch(size=5)))
+    page_conf = {
+        'actor': library.actor,
+        'id': library.url,
+        'page': Paginator(tfs, 3).page(1),
+        'item_serializer': serializers.AudioSerializer,
+    }
+    page = serializers.CollectionPageSerializer(page_conf)
+    r_mock.get(page.data['id'], json=page.data)
+
+    tasks.scan_library_page(
+        library_id=library.pk,
+        page_url=page.data['id'],
+        until=tfs[1].creation_date)
+
+    lts = list(library.tracks.all().order_by('-published_date'))
+    assert len(lts) == 2
+    for i, tf in enumerate(tfs[:1]):
+        assert tf.creation_date == lts[i].published_date
+
+
+def test_clean_federation_music_cache_if_no_listen(preferences, factories):
+    preferences['federation__music_cache_duration'] = 60
+    lt1 = factories['federation.LibraryTrack'](with_audio_file=True)
+    lt2 = factories['federation.LibraryTrack'](with_audio_file=True)
+    lt3 = factories['federation.LibraryTrack'](with_audio_file=True)
+    tf1 = factories['music.TrackFile'](library_track=lt1)
+    tf2 = factories['music.TrackFile'](library_track=lt2)
+    tf3 = factories['music.TrackFile'](library_track=lt3)
+
+    # we listen to the first one, and the second one (but weeks ago)
+    listening1 = factories['history.Listening'](
+        track=tf1.track,
+        creation_date=timezone.now())
+    listening2 = factories['history.Listening'](
+        track=tf2.track,
+        creation_date=timezone.now() - datetime.timedelta(minutes=61))
+
+    tasks.clean_music_cache()
+
+    lt1.refresh_from_db()
+    lt2.refresh_from_db()
+    lt3.refresh_from_db()
+
+    assert bool(lt1.audio_file) is True
+    assert bool(lt2.audio_file) is False
+    assert bool(lt3.audio_file) is False
diff --git a/api/tests/federation/test_views.py b/api/tests/federation/test_views.py
index 0d2ac882fb25ecac154a8426ffc2949ca7f81435..8c5235b8bbeb0f8a42b5a79296d4902067312a2a 100644
--- a/api/tests/federation/test_views.py
+++ b/api/tests/federation/test_views.py
@@ -1,13 +1,17 @@
+from django.core.paginator import Paginator
 from django.urls import reverse
+from django.utils import timezone
 
 import pytest
 
 from funkwhale_api.federation import actors
+from funkwhale_api.federation import activity
+from funkwhale_api.federation import models
 from funkwhale_api.federation import serializers
+from funkwhale_api.federation import utils
 from funkwhale_api.federation import webfinger
 
 
-
 @pytest.mark.parametrize('system_actor', actors.SYSTEM_ACTORS.keys())
 def test_instance_actors(system_actor, db, settings, api_client):
     actor = actors.SYSTEM_ACTORS[system_actor].get_actor_instance()
@@ -17,6 +21,8 @@ def test_instance_actors(system_actor, db, settings, api_client):
     response = api_client.get(url)
     serializer = serializers.ActorSerializer(actor)
 
+    if system_actor == 'library':
+        response.data.pop('url')
     assert response.status_code == 200
     assert response.data == serializer.data
 
@@ -62,3 +68,315 @@ def test_wellknown_webfinger_system(
     assert response.status_code == 200
     assert response['Content-Type'] == 'application/jrd+json'
     assert response.data == serializer.data
+
+
+def test_audio_file_list_requires_authenticated_actor(
+        db, settings, api_client):
+    settings.FEDERATION_MUSIC_NEEDS_APPROVAL = True
+    url = reverse('federation:music:files-list')
+    response = api_client.get(url)
+
+    assert response.status_code == 403
+
+
+def test_audio_file_list_actor_no_page(
+        db, settings, api_client, factories):
+    settings.FEDERATION_MUSIC_NEEDS_APPROVAL = False
+    settings.FEDERATION_COLLECTION_PAGE_SIZE = 2
+    library = actors.SYSTEM_ACTORS['library'].get_actor_instance()
+    tfs = factories['music.TrackFile'].create_batch(size=5)
+    conf = {
+        'id': utils.full_url(reverse('federation:music:files-list')),
+        'page_size': 2,
+        'items': list(reversed(tfs)),  # we order by -creation_date
+        'item_serializer': serializers.AudioSerializer,
+        'actor': library
+    }
+    expected = serializers.PaginatedCollectionSerializer(conf).data
+    url = reverse('federation:music:files-list')
+    response = api_client.get(url)
+
+    assert response.status_code == 200
+    assert response.data == expected
+
+
+def test_audio_file_list_actor_page(
+        db, settings, api_client, factories):
+    settings.FEDERATION_MUSIC_NEEDS_APPROVAL = False
+    settings.FEDERATION_COLLECTION_PAGE_SIZE = 2
+    library = actors.SYSTEM_ACTORS['library'].get_actor_instance()
+    tfs = factories['music.TrackFile'].create_batch(size=5)
+    conf = {
+        'id': utils.full_url(reverse('federation:music:files-list')),
+        'page': Paginator(list(reversed(tfs)), 2).page(2),
+        'item_serializer': serializers.AudioSerializer,
+        'actor': library
+    }
+    expected = serializers.CollectionPageSerializer(conf).data
+    url = reverse('federation:music:files-list')
+    response = api_client.get(url, data={'page': 2})
+
+    assert response.status_code == 200
+    assert response.data == expected
+
+
+def test_audio_file_list_actor_page_exclude_federated_files(
+        db, settings, api_client, factories):
+    settings.FEDERATION_MUSIC_NEEDS_APPROVAL = False
+    library = actors.SYSTEM_ACTORS['library'].get_actor_instance()
+    tfs = factories['music.TrackFile'].create_batch(size=5, federation=True)
+
+    url = reverse('federation:music:files-list')
+    response = api_client.get(url)
+
+    assert response.status_code == 200
+    assert response.data['totalItems'] == 0
+
+
+def test_audio_file_list_actor_page_error(
+        db, settings, api_client, factories):
+    settings.FEDERATION_MUSIC_NEEDS_APPROVAL = False
+    url = reverse('federation:music:files-list')
+    response = api_client.get(url, data={'page': 'nope'})
+
+    assert response.status_code == 400
+
+
+def test_audio_file_list_actor_page_error_too_far(
+        db, settings, api_client, factories):
+    settings.FEDERATION_MUSIC_NEEDS_APPROVAL = False
+    url = reverse('federation:music:files-list')
+    response = api_client.get(url, data={'page': 5000})
+
+    assert response.status_code == 404
+
+
+def test_library_actor_includes_library_link(db, settings, api_client):
+    actor = actors.SYSTEM_ACTORS['library'].get_actor_instance()
+    url = reverse(
+        'federation:instance-actors-detail',
+        kwargs={'actor': 'library'})
+    response = api_client.get(url)
+    expected_links = [
+        {
+            'type': 'Link',
+            'name': 'library',
+            'mediaType': 'application/activity+json',
+            'href': utils.full_url(reverse('federation:music:files-list'))
+        }
+    ]
+    assert response.status_code == 200
+    assert response.data['url'] == expected_links
+
+
+def test_can_fetch_library(superuser_api_client, mocker):
+    result = {'test': 'test'}
+    scan = mocker.patch(
+        'funkwhale_api.federation.library.scan_from_account_name',
+        return_value=result)
+
+    url = reverse('api:v1:federation:libraries-fetch')
+    response = superuser_api_client.get(
+        url, data={'account': 'test@test.library'})
+
+    assert response.status_code == 200
+    assert response.data == result
+    scan.assert_called_once_with('test@test.library')
+
+
+def test_follow_library(superuser_api_client, mocker, factories, r_mock):
+    library_actor = actors.SYSTEM_ACTORS['library'].get_actor_instance()
+    actor = factories['federation.Actor']()
+    follow = {'test': 'follow'}
+    on_commit = mocker.patch(
+        'funkwhale_api.common.utils.on_commit')
+    actor_data = serializers.ActorSerializer(actor).data
+    actor_data['url'] = [{
+        'href': 'https://test.library',
+        'name': 'library',
+        'type': 'Link',
+    }]
+    library_conf = {
+        'id': 'https://test.library',
+        'items': range(10),
+        'actor': actor,
+        'page_size': 5,
+    }
+    library_data = serializers.PaginatedCollectionSerializer(library_conf).data
+    r_mock.get(actor.url, json=actor_data)
+    r_mock.get('https://test.library', json=library_data)
+    data = {
+        'actor': actor.url,
+        'autoimport': False,
+        'federation_enabled': True,
+        'download_files': False,
+    }
+
+    url = reverse('api:v1:federation:libraries-list')
+    response = superuser_api_client.post(
+        url, data)
+
+    assert response.status_code == 201
+
+    follow = models.Follow.objects.get(
+        actor=library_actor,
+        target=actor,
+        approved=None,
+    )
+    library = follow.library
+
+    assert response.data == serializers.APILibraryCreateSerializer(
+        library).data
+
+    on_commit.assert_called_once_with(
+        activity.deliver,
+        serializers.FollowSerializer(follow).data,
+        on_behalf_of=library_actor,
+        to=[actor.url]
+    )
+
+
+def test_can_list_system_actor_following(factories, superuser_api_client):
+    library_actor = actors.SYSTEM_ACTORS['library'].get_actor_instance()
+    follow1 = factories['federation.Follow'](actor=library_actor)
+    follow2 = factories['federation.Follow']()
+
+    url = reverse('api:v1:federation:libraries-following')
+    response = superuser_api_client.get(url)
+
+    assert response.status_code == 200
+    assert response.data['results'] == [
+        serializers.APIFollowSerializer(follow1).data
+    ]
+
+
+def test_can_list_system_actor_followers(factories, superuser_api_client):
+    library_actor = actors.SYSTEM_ACTORS['library'].get_actor_instance()
+    follow1 = factories['federation.Follow'](actor=library_actor)
+    follow2 = factories['federation.Follow'](target=library_actor)
+
+    url = reverse('api:v1:federation:libraries-followers')
+    response = superuser_api_client.get(url)
+
+    assert response.status_code == 200
+    assert response.data['results'] == [
+        serializers.APIFollowSerializer(follow2).data
+    ]
+
+
+def test_can_list_libraries(factories, superuser_api_client):
+    library1 = factories['federation.Library']()
+    library2 = factories['federation.Library']()
+
+    url = reverse('api:v1:federation:libraries-list')
+    response = superuser_api_client.get(url)
+
+    assert response.status_code == 200
+    assert response.data['results'] == [
+        serializers.APILibrarySerializer(library1).data,
+        serializers.APILibrarySerializer(library2).data,
+    ]
+
+
+def test_can_detail_library(factories, superuser_api_client):
+    library = factories['federation.Library']()
+
+    url = reverse(
+        'api:v1:federation:libraries-detail',
+        kwargs={'uuid': str(library.uuid)})
+    response = superuser_api_client.get(url)
+
+    assert response.status_code == 200
+    assert response.data == serializers.APILibrarySerializer(library).data
+
+
+def test_can_patch_library(factories, superuser_api_client):
+    library = factories['federation.Library']()
+    data = {
+        'federation_enabled': not library.federation_enabled,
+        'download_files': not library.download_files,
+        'autoimport': not library.autoimport,
+    }
+    url = reverse(
+        'api:v1:federation:libraries-detail',
+        kwargs={'uuid': str(library.uuid)})
+    response = superuser_api_client.patch(url, data)
+
+    assert response.status_code == 200
+    library.refresh_from_db()
+
+    for k, v in data.items():
+        assert getattr(library, k) == v
+
+
+def test_scan_library(factories, mocker, superuser_api_client):
+    scan = mocker.patch(
+        'funkwhale_api.federation.tasks.scan_library.delay',
+        return_value=mocker.Mock(id='id'))
+    library = factories['federation.Library']()
+    now = timezone.now()
+    data = {
+        'until': now,
+    }
+    url = reverse(
+        'api:v1:federation:libraries-scan',
+        kwargs={'uuid': str(library.uuid)})
+    response = superuser_api_client.post(url, data)
+
+    assert response.status_code == 200
+    assert response.data == {'task': 'id'}
+    scan.assert_called_once_with(
+        library_id=library.pk,
+        until=now
+    )
+
+
+def test_list_library_tracks(factories, superuser_api_client):
+    library = factories['federation.Library']()
+    lts = list(reversed(factories['federation.LibraryTrack'].create_batch(
+        size=5, library=library)))
+    factories['federation.LibraryTrack'].create_batch(size=5)
+    url = reverse('api:v1:federation:library-tracks-list')
+    response = superuser_api_client.get(url, {'library': library.uuid})
+
+    assert response.status_code == 200
+    assert response.data == {
+        'results': serializers.APILibraryTrackSerializer(lts, many=True).data,
+        'count': 5,
+        'previous': None,
+        'next': None,
+    }
+
+
+def test_can_update_follow_status(factories, superuser_api_client, mocker):
+    patched_accept = mocker.patch(
+        'funkwhale_api.federation.activity.accept_follow'
+    )
+    library_actor = actors.SYSTEM_ACTORS['library'].get_actor_instance()
+    follow = factories['federation.Follow'](target=library_actor)
+
+    payload = {
+        'follow': follow.pk,
+        'approved': True
+    }
+    url = reverse('api:v1:federation:libraries-followers')
+    response = superuser_api_client.patch(url, payload)
+    follow.refresh_from_db()
+
+    assert response.status_code == 200
+    assert follow.approved is True
+    patched_accept.assert_called_once_with(follow)
+
+
+def test_can_filter_pending_follows(factories, superuser_api_client):
+    library_actor = actors.SYSTEM_ACTORS['library'].get_actor_instance()
+    follow = factories['federation.Follow'](
+        target=library_actor,
+        approved=True)
+
+    params = {'pending': True}
+    url = reverse('api:v1:federation:libraries-followers')
+    response = superuser_api_client.get(url, params)
+
+    assert response.status_code == 200
+    assert len(response.data['results']) == 0
diff --git a/api/tests/federation/test_webfinger.py b/api/tests/federation/test_webfinger.py
index 96258455ae6fe1f60e330d7768a58d6e94fc91fa..4b8dca207ad57b36ee878e68009b0a778b72bcfd 100644
--- a/api/tests/federation/test_webfinger.py
+++ b/api/tests/federation/test_webfinger.py
@@ -40,3 +40,29 @@ def test_webfinger_clean_acct_errors(resource, message, settings):
         webfinger.clean_resource(resource)
 
         assert message == str(excinfo)
+
+
+def test_webfinger_get_resource(r_mock):
+    resource = 'acct:test@test.webfinger'
+    payload = {
+        'subject': resource,
+        'aliases': ['https://test.webfinger'],
+        'links': [
+            {
+                'rel': 'self',
+                'type': 'application/activity+json',
+                'href': 'https://test.webfinger/user/test'
+            }
+        ]
+    }
+    r_mock.get(
+        'https://test.webfinger/.well-known/webfinger?resource={}'.format(
+            resource
+        ),
+        json=payload
+    )
+
+    data = webfinger.get_resource('acct:test@test.webfinger')
+
+    assert data['actor_url'] == 'https://test.webfinger/user/test'
+    assert data['subject'] == resource
diff --git a/api/tests/music/test_import.py b/api/tests/music/test_import.py
index 0f709e81f508fcb0e4e2ee06e92991a2f907cdff..2f22ed69ad8db3ffb6ec098d606b194fbdb8ab3e 100644
--- a/api/tests/music/test_import.py
+++ b/api/tests/music/test_import.py
@@ -1,7 +1,12 @@
 import json
+import pytest
 
 from django.urls import reverse
 
+from funkwhale_api.federation import actors
+from funkwhale_api.federation import serializers as federation_serializers
+from funkwhale_api.music import tasks
+
 
 def test_create_import_can_bind_to_request(
         artists, albums, mocker, factories, superuser_api_client):
@@ -33,3 +38,196 @@ def test_create_import_can_bind_to_request(
     batch = request.import_batches.latest('id')
 
     assert batch.import_request == request
+
+
+def test_import_job_from_federation_no_musicbrainz(factories):
+    lt = factories['federation.LibraryTrack'](
+        artist_name='Hello',
+        album_title='World',
+        title='Ping',
+    )
+    job = factories['music.ImportJob'](
+        federation=True,
+        library_track=lt,
+    )
+
+    tasks.import_job_run(import_job_id=job.pk)
+    job.refresh_from_db()
+
+    tf = job.track_file
+    assert tf.mimetype == lt.audio_mimetype
+    assert tf.library_track == job.library_track
+    assert tf.track.title == 'Ping'
+    assert tf.track.artist.name == 'Hello'
+    assert tf.track.album.title == 'World'
+
+
+def test_import_job_from_federation_musicbrainz_recording(factories, mocker):
+    t = factories['music.Track']()
+    track_from_api = mocker.patch(
+        'funkwhale_api.music.models.Track.get_or_create_from_api',
+        return_value=t)
+    lt = factories['federation.LibraryTrack'](
+        metadata__recording__musicbrainz=True,
+        artist_name='Hello',
+        album_title='World',
+    )
+    job = factories['music.ImportJob'](
+        federation=True,
+        library_track=lt,
+    )
+
+    tasks.import_job_run(import_job_id=job.pk)
+    job.refresh_from_db()
+
+    tf = job.track_file
+    assert tf.mimetype == lt.audio_mimetype
+    assert tf.library_track == job.library_track
+    assert tf.track == t
+    track_from_api.assert_called_once_with(
+        mbid=lt.metadata['recording']['musicbrainz_id'])
+
+
+def test_import_job_from_federation_musicbrainz_release(factories, mocker):
+    a = factories['music.Album']()
+    album_from_api = mocker.patch(
+        'funkwhale_api.music.models.Album.get_or_create_from_api',
+        return_value=a)
+    lt = factories['federation.LibraryTrack'](
+        metadata__release__musicbrainz=True,
+        artist_name='Hello',
+        title='Ping',
+    )
+    job = factories['music.ImportJob'](
+        federation=True,
+        library_track=lt,
+    )
+
+    tasks.import_job_run(import_job_id=job.pk)
+    job.refresh_from_db()
+
+    tf = job.track_file
+    assert tf.mimetype == lt.audio_mimetype
+    assert tf.library_track == job.library_track
+    assert tf.track.title == 'Ping'
+    assert tf.track.artist == a.artist
+    assert tf.track.album == a
+
+    album_from_api.assert_called_once_with(
+        mbid=lt.metadata['release']['musicbrainz_id'])
+
+
+def test_import_job_from_federation_musicbrainz_artist(factories, mocker):
+    a = factories['music.Artist']()
+    artist_from_api = mocker.patch(
+        'funkwhale_api.music.models.Artist.get_or_create_from_api',
+        return_value=a)
+    lt = factories['federation.LibraryTrack'](
+        metadata__artist__musicbrainz=True,
+        album_title='World',
+        title='Ping',
+    )
+    job = factories['music.ImportJob'](
+        federation=True,
+        library_track=lt,
+    )
+
+    tasks.import_job_run(import_job_id=job.pk)
+    job.refresh_from_db()
+
+    tf = job.track_file
+    assert tf.mimetype == lt.audio_mimetype
+    assert tf.library_track == job.library_track
+
+    assert tf.track.title == 'Ping'
+    assert tf.track.artist == a
+    assert tf.track.album.artist == a
+    assert tf.track.album.title == 'World'
+
+    artist_from_api.assert_called_once_with(
+        mbid=lt.metadata['artist']['musicbrainz_id'])
+
+
+def test_import_job_run_triggers_notifies_followers(
+        factories, mocker, tmpfile):
+    mocker.patch(
+        'funkwhale_api.downloader.download',
+        return_value={'audio_file_path': tmpfile.name})
+    mocked_notify = mocker.patch(
+        'funkwhale_api.music.tasks.import_batch_notify_followers.delay')
+    batch = factories['music.ImportBatch']()
+    job = factories['music.ImportJob'](
+        finished=True, batch=batch)
+    track = factories['music.Track'](mbid=job.mbid)
+
+    batch.update_status()
+    batch.refresh_from_db()
+
+    assert batch.status == 'finished'
+
+    mocked_notify.assert_called_once_with(import_batch_id=batch.pk)
+
+
+def test_import_batch_notifies_followers_skip_on_disabled_federation(
+        settings, factories, mocker):
+    mocked_deliver = mocker.patch('funkwhale_api.federation.activity.deliver')
+    batch = factories['music.ImportBatch'](finished=True)
+    settings.FEDERATION_ENABLED = False
+    tasks.import_batch_notify_followers(import_batch_id=batch.pk)
+
+    mocked_deliver.assert_not_called()
+
+
+def test_import_batch_notifies_followers_skip_on_federation_import(
+        factories, mocker):
+    mocked_deliver = mocker.patch('funkwhale_api.federation.activity.deliver')
+    batch = factories['music.ImportBatch'](finished=True, federation=True)
+    tasks.import_batch_notify_followers(import_batch_id=batch.pk)
+
+    mocked_deliver.assert_not_called()
+
+
+def test_import_batch_notifies_followers(
+        factories, mocker):
+    library_actor = actors.SYSTEM_ACTORS['library'].get_actor_instance()
+
+    f1 = factories['federation.Follow'](approved=True, target=library_actor)
+    f2 = factories['federation.Follow'](approved=False, target=library_actor)
+    f3 = factories['federation.Follow']()
+
+    mocked_deliver = mocker.patch('funkwhale_api.federation.activity.deliver')
+    batch = factories['music.ImportBatch']()
+    job1 = factories['music.ImportJob'](
+        finished=True, batch=batch)
+    job2 = factories['music.ImportJob'](
+        finished=True, federation=True, batch=batch)
+    job3 = factories['music.ImportJob'](
+        status='pending', batch=batch)
+
+    batch.status = 'finished'
+    batch.save()
+    tasks.import_batch_notify_followers(import_batch_id=batch.pk)
+
+    # only f1 match the requirements to be notified
+    # and only job1 is a non federated track with finished import
+    expected = {
+        '@context': federation_serializers.AP_CONTEXT,
+        'actor': library_actor.url,
+        'type': 'Create',
+        'id': batch.get_federation_url(),
+        'to': [f1.actor.url],
+        'object': federation_serializers.CollectionSerializer(
+            {
+                'id': batch.get_federation_url(),
+                'items': [job1.track_file],
+                'actor': library_actor,
+                'item_serializer': federation_serializers.AudioSerializer
+            }
+        ).data
+    }
+
+    mocked_deliver.assert_called_once_with(
+        expected,
+        on_behalf_of=library_actor,
+        to=[f1.actor.url]
+    )
diff --git a/api/tests/music/test_permissions.py b/api/tests/music/test_permissions.py
new file mode 100644
index 0000000000000000000000000000000000000000..d36f37886d9930883643126642ab278eee8acf93
--- /dev/null
+++ b/api/tests/music/test_permissions.py
@@ -0,0 +1,71 @@
+from rest_framework.views import APIView
+
+from funkwhale_api.federation import actors
+from funkwhale_api.music import permissions
+
+
+def test_list_permission_no_protect(anonymous_user, api_request, settings):
+    settings.PROTECT_AUDIO_FILES = False
+    view = APIView.as_view()
+    permission = permissions.Listen()
+    request = api_request.get('/')
+    assert permission.has_permission(request, view) is True
+
+
+def test_list_permission_protect_anonymous(
+        anonymous_user, api_request, settings):
+    settings.PROTECT_AUDIO_FILES = True
+    view = APIView.as_view()
+    permission = permissions.Listen()
+    request = api_request.get('/')
+    assert permission.has_permission(request, view) is False
+
+
+def test_list_permission_protect_authenticated(
+        factories, api_request, settings):
+    settings.PROTECT_AUDIO_FILES = True
+    user = factories['users.User']()
+    view = APIView.as_view()
+    permission = permissions.Listen()
+    request = api_request.get('/')
+    setattr(request, 'user', user)
+    assert permission.has_permission(request, view) is True
+
+
+def test_list_permission_protect_not_following_actor(
+        factories, api_request, settings):
+    settings.PROTECT_AUDIO_FILES = True
+    actor = factories['federation.Actor']()
+    view = APIView.as_view()
+    permission = permissions.Listen()
+    request = api_request.get('/')
+    setattr(request, 'actor', actor)
+    assert permission.has_permission(request, view) is False
+
+
+def test_list_permission_protect_following_actor(
+        factories, api_request, settings):
+    settings.PROTECT_AUDIO_FILES = True
+    library_actor = actors.SYSTEM_ACTORS['library'].get_actor_instance()
+    follow = factories['federation.Follow'](
+        approved=True, target=library_actor)
+    view = APIView.as_view()
+    permission = permissions.Listen()
+    request = api_request.get('/')
+    setattr(request, 'actor', follow.actor)
+
+    assert permission.has_permission(request, view) is True
+
+
+def test_list_permission_protect_following_actor_not_approved(
+        factories, api_request, settings):
+    settings.PROTECT_AUDIO_FILES = True
+    library_actor = actors.SYSTEM_ACTORS['library'].get_actor_instance()
+    follow = factories['federation.Follow'](
+        approved=False, target=library_actor)
+    view = APIView.as_view()
+    permission = permissions.Listen()
+    request = api_request.get('/')
+    setattr(request, 'actor', follow.actor)
+
+    assert permission.has_permission(request, view) is False
diff --git a/api/tests/music/test_views.py b/api/tests/music/test_views.py
index 2956046168ca30487976512518bedce919752f2e..81f34fbe9d95d41e198ef783746df65dcdbb0561 100644
--- a/api/tests/music/test_views.py
+++ b/api/tests/music/test_views.py
@@ -1,6 +1,10 @@
+import io
 import pytest
 
+from django.urls import reverse
+
 from funkwhale_api.music import views
+from funkwhale_api.federation import actors
 
 
 @pytest.mark.parametrize('param,expected', [
@@ -43,3 +47,65 @@ def test_album_view_filter_listenable(
     queryset = view.filter_queryset(view.get_queryset())
 
     assert list(queryset) == expected
+
+
+def test_can_serve_track_file_as_remote_library(
+        factories, authenticated_actor, settings, api_client):
+    settings.PROTECT_AUDIO_FILES = True
+    library_actor = actors.SYSTEM_ACTORS['library'].get_actor_instance()
+    follow = factories['federation.Follow'](
+        approved=True,
+        actor=authenticated_actor,
+        target=library_actor)
+
+    track_file = factories['music.TrackFile']()
+    response = api_client.get(track_file.path)
+
+    assert response.status_code == 200
+    assert response['X-Accel-Redirect'] == "{}{}".format(
+        settings.PROTECT_FILES_PATH,
+        track_file.audio_file.url)
+
+
+def test_can_serve_track_file_as_remote_library_deny_not_following(
+        factories, authenticated_actor, settings, api_client):
+    settings.PROTECT_AUDIO_FILES = True
+    track_file = factories['music.TrackFile']()
+    response = api_client.get(track_file.path)
+
+    assert response.status_code == 403
+
+
+def test_can_proxy_remote_track(
+        factories, settings, api_client, r_mock):
+    settings.PROTECT_AUDIO_FILES = False
+    track_file = factories['music.TrackFile'](federation=True)
+
+    r_mock.get(track_file.library_track.audio_url, body=io.BytesIO(b'test'))
+    response = api_client.get(track_file.path)
+
+    library_track = track_file.library_track
+    library_track.refresh_from_db()
+    assert response.status_code == 200
+    assert response['X-Accel-Redirect'] == "{}{}".format(
+        settings.PROTECT_FILES_PATH,
+        library_track.audio_file.url)
+    assert library_track.audio_file.read() == b'test'
+
+
+def test_can_create_import_from_federation_tracks(
+        factories, superuser_api_client, mocker):
+    lts = factories['federation.LibraryTrack'].create_batch(size=5)
+    mocker.patch('funkwhale_api.music.tasks.import_job_run')
+
+    payload = {
+        'library_tracks': [l.pk for l in lts]
+    }
+    url = reverse('api:v1:submit-federation')
+    response = superuser_api_client.post(url, payload)
+
+    assert response.status_code == 201
+    batch = superuser_api_client.user.imports.latest('id')
+    assert batch.jobs.count() == 5
+    for i, job in enumerate(batch.jobs.all()):
+        assert job.library_track == lts[i]
diff --git a/deploy/docker-compose.yml b/deploy/docker-compose.yml
index 69d5e17214e09b01b5d9d262f088e7d8508ae21b..cc4f357cad229768d1b98d8b104c7aa8f2c691d5 100644
--- a/deploy/docker-compose.yml
+++ b/deploy/docker-compose.yml
@@ -53,3 +53,19 @@ services:
     links:
       - postgres
       - redis
+
+  # If you want to have the nginx proxy managed by docker for some reason
+  # (i.e. if you use apache as a proxy on your host),
+  # you can uncomment the following lines.
+  # nginx:
+  #   image: nginx
+  #   links:
+  #     - api
+  #   volumes:
+  #     - ./nginx.conf:/etc/nginx/conf.d/funkwhale.conf:ro
+  #     - ./funkwhale_proxy.conf:/etc/nginx/funkwhale_proxy.conf:ro
+  #     - ./data/media:/srv/funkwhale/data/media:ro
+  #     - ./front/dist:/srv/funkwhale/front/dist:ro
+  #     - ./data/static:/srv/funkwhale/data/static/:ro
+  #   ports:
+  #     - "127.0.0.1:5001:80"
diff --git a/deploy/env.prod.sample b/deploy/env.prod.sample
index a016b34c7eea37e0272fb8b08e5afcfd6a9c085f..9e9938500823e538cced62b88280f7ef67824ccb 100644
--- a/deploy/env.prod.sample
+++ b/deploy/env.prod.sample
@@ -85,3 +85,12 @@ API_AUTHENTICATION_REQUIRED=True
 # This will help us detect and correct bugs
 RAVEN_ENABLED=false
 RAVEN_DSN=https://44332e9fdd3d42879c7d35bf8562c6a4:0062dc16a22b41679cd5765e5342f716@sentry.eliotberriot.com/5
+
+# This settings enable/disable federation on the instance level
+FEDERATION_ENABLED=True
+# This setting decide wether music library is shared automatically
+# to followers or if it requires manual approval before.
+# FEDERATION_MUSIC_NEEDS_APPROVAL=False
+# means anyone can subscribe to your library and import your file,
+# use with caution.
+FEDERATION_MUSIC_NEEDS_APPROVAL=True
diff --git a/deploy/funkwhale-beat.service b/deploy/funkwhale-beat.service
new file mode 100644
index 0000000000000000000000000000000000000000..209896dd4272ca0f7ab3b683fa95a514b67f5290
--- /dev/null
+++ b/deploy/funkwhale-beat.service
@@ -0,0 +1,14 @@
+[Unit]
+Description=Funkwhale celery beat process
+After=redis.service postgresql.service
+PartOf=funkwhale.target
+
+[Service]
+User=funkwhale
+# adapt this depending on the path of your funkwhale installation
+WorkingDirectory=/srv/funkwhale/api
+EnvironmentFile=/srv/funkwhale/config/.env
+ExecStart=/srv/funkwhale/virtualenv/bin/celery -A funkwhale_api.taskapp beat -l INFO
+
+[Install]
+WantedBy=multi-user.target
diff --git a/deploy/funkwhale.target b/deploy/funkwhale.target
index a920c7e34c67d31c8bb295c254d9cddc899da2a2..a09e381c4f6553dd482b2f4edd30fcfc3e851f74 100644
--- a/deploy/funkwhale.target
+++ b/deploy/funkwhale.target
@@ -1,3 +1,3 @@
 [Unit]
 Description=Funkwhale
-Wants=funkwhale-server.service funkwhale-worker.service
+Wants=funkwhale-server.service funkwhale-worker.service funkwhale-beat.service
diff --git a/dev.yml b/dev.yml
index c0470a2ab6f6172127e90fa9d3911b0cae9b33db..2df7b44e60100b4812db940f45fb8baf14da2544 100644
--- a/dev.yml
+++ b/dev.yml
@@ -10,21 +10,40 @@ services:
       - "HOST=0.0.0.0"
       - "WEBPACK_DEVSERVER_PORT=${WEBPACK_DEVSERVER_PORT-8080}"
     ports:
-      - "${WEBPACK_DEVSERVER_PORT-8080}:${WEBPACK_DEVSERVER_PORT-8080}"
+      - "${WEBPACK_DEVSERVER_PORT_BINDING-8080:}${WEBPACK_DEVSERVER_PORT-8080}"
     volumes:
       - './front:/app'
+      - '/app/node_modules'
+      - './po:/po'
+    networks:
+      - federation
+      - internal
+    labels:
+      traefik.backend: "${COMPOSE_PROJECT_NAME-node1}"
+      traefik.frontend.rule: "Host: ${COMPOSE_PROJECT_NAME-node1}.funkwhale.test"
+      traefik.enable: 'true'
+      traefik.federation.protocol: 'http'
+      traefik.federation.port: "${WEBPACK_DEVSERVER_PORT-8080}"
 
   postgres:
     env_file:
       - .env.dev
       - .env
     image: postgres
+    volumes:
+      - "./data/${COMPOSE_PROJECT_NAME-node1}/postgres:/var/lib/postgresql/data"
+    networks:
+      - internal
 
   redis:
     env_file:
       - .env.dev
       - .env
     image: redis:3.0
+    volumes:
+      - "./data/${COMPOSE_PROJECT_NAME-node1}/redis:/data"
+    networks:
+      - internal
 
   celeryworker:
     env_file:
@@ -38,11 +57,17 @@ services:
      - redis
     command: celery -A funkwhale_api.taskapp worker -l debug
     environment:
+      - "FUNKWHALE_HOSTNAME=${FUNKWHALE_HOSTNAME-localhost}"
+      - "FUNKWHALE_HOSTNAME_SUFFIX=funkwhale.test"
+      - "FUNKWHALE_HOSTNAME_PREFIX=${COMPOSE_PROJECT_NAME}"
+      - "FUNKWHALE_PROTOCOL=${FUNKWHALE_PROTOCOL-http}"
       - "DATABASE_URL=postgresql://postgres@postgres/postgres"
       - "CACHE_URL=redis://redis:6379/0"
     volumes:
       - ./api:/app
       - ./data/music:/music
+    networks:
+      - internal
   api:
     env_file:
       - .env.dev
@@ -55,12 +80,17 @@ services:
       - ./api:/app
       - ./data/music:/music
     environment:
+      - "FUNKWHALE_HOSTNAME=${FUNKWHALE_HOSTNAME-localhost}"
+      - "FUNKWHALE_HOSTNAME_SUFFIX=funkwhale.test"
+      - "FUNKWHALE_HOSTNAME_PREFIX=${COMPOSE_PROJECT_NAME}"
+      - "FUNKWHALE_PROTOCOL=${FUNKWHALE_PROTOCOL-http}"
       - "DATABASE_URL=postgresql://postgres@postgres/postgres"
       - "CACHE_URL=redis://redis:6379/0"
     links:
       - postgres
       - redis
-
+    networks:
+      - internal
   nginx:
     command: /entrypoint.sh
     env_file:
@@ -69,6 +99,8 @@ services:
     image: nginx
     environment:
       - "WEBPACK_DEVSERVER_PORT=${WEBPACK_DEVSERVER_PORT-8080}"
+      - "COMPOSE_PROJECT_NAME=${COMPOSE_PROJECT_NAME- }"
+      - "FUNKWHALE_HOSTNAME=${FUNKWHALE_HOSTNAME-localhost}"
     links:
       - api
       - front
@@ -78,8 +110,9 @@ services:
       - ./deploy/funkwhale_proxy.conf:/etc/nginx/funkwhale_proxy.conf.template:ro
       - ./api/funkwhale_api/media:/protected/media
     ports:
-      - "0.0.0.0:6001:6001"
-
+      - "6001"
+    networks:
+      - internal
   docs:
     build: docs
     command: python serve.py
@@ -88,3 +121,9 @@ services:
     ports:
       - '35730:35730'
       - '8001:8001'
+
+networks:
+  internal:
+  federation:
+    external:
+      name: federation
diff --git a/docker/nginx/entrypoint.sh b/docker/nginx/entrypoint.sh
index 1819acf1cf579cb3e0d70b754d06f2b6fc4959c6..14e072a7e986c6b2b1cb7605efee77cda8985215 100755
--- a/docker/nginx/entrypoint.sh
+++ b/docker/nginx/entrypoint.sh
@@ -1,10 +1,17 @@
 #!/bin/bash -eux
-FIRST_HOST=$(echo ${DJANGO_ALLOWED_HOSTS} | cut -d, -f1)
+
+FORWARDED_PORT="$WEBPACK_DEVSERVER_PORT"
+COMPOSE_PROJECT_NAME="${COMPOSE_PROJECT_NAME// /}"
+if [ -n "$COMPOSE_PROJECT_NAME" ]; then
+  echo
+    FUNKWHALE_HOSTNAME="$COMPOSE_PROJECT_NAME.funkwhale.test"
+    FORWARDED_PORT="443"
+fi
 echo "Copying template file..."
 cp /etc/nginx/funkwhale_proxy.conf{.template,}
-sed -i "s/X-Forwarded-Host \$host:\$server_port/X-Forwarded-Host ${FIRST_HOST}:${WEBPACK_DEVSERVER_PORT}/" /etc/nginx/funkwhale_proxy.conf
-sed -i "s/proxy_set_header Host \$host/proxy_set_header Host ${FIRST_HOST}/" /etc/nginx/funkwhale_proxy.conf
-sed -i "s/proxy_set_header X-Forwarded-Port \$server_port/proxy_set_header X-Forwarded-Port ${WEBPACK_DEVSERVER_PORT}/" /etc/nginx/funkwhale_proxy.conf
+sed -i "s/X-Forwarded-Host \$host:\$server_port/X-Forwarded-Host ${FUNKWHALE_HOSTNAME}:${FORWARDED_PORT}/" /etc/nginx/funkwhale_proxy.conf
+sed -i "s/proxy_set_header Host \$host/proxy_set_header Host ${FUNKWHALE_HOSTNAME}/" /etc/nginx/funkwhale_proxy.conf
+sed -i "s/proxy_set_header X-Forwarded-Port \$server_port/proxy_set_header X-Forwarded-Port ${FORWARDED_PORT}/" /etc/nginx/funkwhale_proxy.conf
 
 cat /etc/nginx/funkwhale_proxy.conf
 nginx -g "daemon off;"
diff --git a/docker/ssl/test.crt b/docker/ssl/test.crt
new file mode 100644
index 0000000000000000000000000000000000000000..e4d3eefb8442e78d3ce5dab60ea9393bea3b6258
--- /dev/null
+++ b/docker/ssl/test.crt
@@ -0,0 +1,22 @@
+-----BEGIN CERTIFICATE-----
+MIIDljCCAn6gAwIBAgIJAOA/w9NwL3aMMA0GCSqGSIb3DQEBCwUAMGAxCzAJBgNV
+BAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBX
+aWRnaXRzIFB0eSBMdGQxGTAXBgNVBAMMECouZnVua3doYWxlLnRlc3QwHhcNMTgw
+NDA4MTMwNDAzWhcNMjgwNDA1MTMwNDAzWjBgMQswCQYDVQQGEwJBVTETMBEGA1UE
+CAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50ZXJuZXQgV2lkZ2l0cyBQdHkgTHRk
+MRkwFwYDVQQDDBAqLmZ1bmt3aGFsZS50ZXN0MIIBIjANBgkqhkiG9w0BAQEFAAOC
+AQ8AMIIBCgKCAQEAyGqRLEMFs1mpRwauTicIRj2zwBUe6JMNRbIvOUkaj2KY6avA
+7tiNti/ygBoTyJl2JK3mmLqxElqedpMhjVvYde/PyjXoZ+0Vq4FWv89LV6ZM/Scf
+TCIYwWF1ppi6GYFmU3WCIMISkKiPBtMArB0oZxiUWLmkyd8jih2wnQOpkQ20FfG0
+CtlrKlQKyAe7X3zPuqGfaMUN7J4w9g3/SC66YulbAtI1/Z4tuG8J4m2RC6jH1hVy
+364l3ifEC+m9Kax/ystfu/mkLdyQgRfOZTNf2JhS3BL8zpoWMXFK+4+7TYisrV1h
+0pzIAsoQeBB+cFOOFEwRAv0FxSWnZ+/shjnwbwIDAQABo1MwUTAdBgNVHQ4EFgQU
+sULmofttRyWUMM93IsD8jBvyCd4wHwYDVR0jBBgwFoAUsULmofttRyWUMM93IsD8
+jBvyCd4wDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAUg/fiXut
+hW6fDx9f0JdB4uLiLnv8tDP35ackLLapFJhXtflIXcqCzxStQ46nMs1wjaZPb+ws
+pLULzvTKTxJbu+JYc2nvis4m2oSFczJ3S9tgug4Ppv8yS7N1pp7kfjOvBjgh6sYW
+p+Ctb5r8qvgvT9yDTeCnsqktb/OkRHlHwhRYfnuxh+96s4mzifqFUP4uCCcFYPTc
+RE0Ag3oI5sHOdDk/cdYE5PGQPjSP6gzn0lsrz1Q3x1C8+txSHzsJnvS3Ost+dwcy
+JSjDBXauy9cZv93Voevcl16Ioo7trtkp4dwAoep52vOT/KMkJ4zm19msV3BP4wMa
+BUqrV2F7twD5zw==
+-----END CERTIFICATE-----
diff --git a/docker/ssl/test.key b/docker/ssl/test.key
new file mode 100644
index 0000000000000000000000000000000000000000..669e5f7004be843fd49e72a65166efac9cbcc526
--- /dev/null
+++ b/docker/ssl/test.key
@@ -0,0 +1,28 @@
+-----BEGIN PRIVATE KEY-----
+MIIEwAIBADANBgkqhkiG9w0BAQEFAASCBKowggSmAgEAAoIBAQDIapEsQwWzWalH
+Bq5OJwhGPbPAFR7okw1Fsi85SRqPYpjpq8Du2I22L/KAGhPImXYkreaYurESWp52
+kyGNW9h178/KNehn7RWrgVa/z0tXpkz9Jx9MIhjBYXWmmLoZgWZTdYIgwhKQqI8G
+0wCsHShnGJRYuaTJ3yOKHbCdA6mRDbQV8bQK2WsqVArIB7tffM+6oZ9oxQ3snjD2
+Df9ILrpi6VsC0jX9ni24bwnibZELqMfWFXLfriXeJ8QL6b0prH/Ky1+7+aQt3JCB
+F85lM1/YmFLcEvzOmhYxcUr7j7tNiKytXWHSnMgCyhB4EH5wU44UTBEC/QXFJadn
+7+yGOfBvAgMBAAECggEBAMVB3lEqRloYTbxSnwzc7g/0ew77usg+tDl8/23qvfGS
+od6b5fEvw4sl9hCPmhk+skG3x9dbKR1fg8hBWCzB0XOC7YmhNXXUrBd53eA8L3O9
+gtlHwE424Ra0zg+DEug3rHdImSOU4KDwxpV46Jh+ul1+m8QYNFFdBqXSQxrHmAXj
+MQ6++rjoJ+bhucmjBouzMYXHTGhdae3kjDFrFJ4cUsH6F03NcDwS+AmZxa/DWQ/H
+SoBQBeLoE6I1aKhLgY91yO1e7CtSzS2GFCODReN4b3cylaR7jE7Mg87TZcga6Wfa
+Xcd120VVlVq6HmZc/Xob7aUim3AuY2er8bcvmg1XOsECgYEA5EMM5UlpLdNWv1hp
+5IMvkeCbXtLJ3IOHO0xLkFdx0CxaR9TyAAqIrSh1t9rFhYqLUNiOdMc2TqrvdgEU
+B/QZrAevWRc5sjPvFXmYeWSCi/tjRgQh4jClWDX/TlfAlP55z2BFyMPMX6//WbBQ
+5aL9xymTymzFFcaE8EytT5Jz8rUCgYEA4MVF3IkaQepl6H1gf2T6ev+MtGk9AGg9
+DSJpio7hfMcY5X3NrTJJFF9DJFXqfo3ILOMyUpIUHqkCGKXil0n9ypLp4vq7l+6c
+m1gtKFXh7uKAV4XtSnR0nuK/N10JJp2HbbFYGlziRaa1iEPAFvLDQHu4jyf5sXyV
+HvreuQgGWRMCgYEAlUaQKWaP5UsfoPUGE04DjwfvM9zv7EkL6CimBhhZswU+aVmG
+haZd6bfa/EiTAhkvsMheqVoaVuoMvgRIgEcPfuRrtPyuW68A/O9PWpvzj+3v5zsO
+maisiPqPI0HaDNY6/PZ9zKTXhABKIvJehT7JbjTvlOL7JJl2GNxcPvyM3T0CgYEA
+tnVtUKi69+ce8qtUOhXufwoTXiBPtJTpelAE/MUfpfq46xJEc+PuDuuFxWk5AaJ2
+bHnBz+VlD76CRR/j4IvfySGZWvfOcHbyCeh6P9P3o8OaC3JcPaRrRs8qCfcsBny6
+AwGDU2MzCvdZRVQ6CmbmuOG13//DYaCQLKXZRrqM7KECgYEAxDsqtyHA/a38UhS8
+iQ8HqrZp8CuzJoJw/QILvzjojD1cvmwF73RrPEpRfEaLWVQGQ5F1IlHk/009C5zy
+eUT4ZaPxLem6khBf7pn3xXaVBGZsYoltek5sUBsu/jA+4Sw6bcUmhBRBCs98JGpR
+DVJtvOTk9aGW8M8UbgqwW+e/6ng=
+-----END PRIVATE KEY-----
diff --git a/docker/traefik.toml b/docker/traefik.toml
new file mode 100644
index 0000000000000000000000000000000000000000..85da2ea7288a9609506d925178f79fc9a6fa4be1
--- /dev/null
+++ b/docker/traefik.toml
@@ -0,0 +1,26 @@
+defaultEntryPoints = ["http", "https"]
+
+################################################################
+# Web configuration backend
+################################################################
+[web]
+address = ":8040"
+################################################################
+# Docker configuration backend
+################################################################
+[docker]
+domain = "funkwhale.test"
+watch = true
+exposedbydefault = false
+
+[entryPoints]
+  [entryPoints.http]
+  address = ":80"
+    [entryPoints.http.redirect]
+    entryPoint = "https"
+  [entryPoints.https]
+  address = ":443"
+    [entryPoints.https.tls]
+      [[entryPoints.https.tls.certificates]]
+      certFile = "/ssl/traefik.crt"
+      keyFile = "/ssl/traefik.key"
diff --git a/docker/traefik.yml b/docker/traefik.yml
new file mode 100644
index 0000000000000000000000000000000000000000..0b15b32904fec3aea7276c31c1a660ca51f37f64
--- /dev/null
+++ b/docker/traefik.yml
@@ -0,0 +1,22 @@
+version: '2.1'
+
+services:
+  traefik:
+    image: traefik:alpine
+    volumes:
+      - /var/run/docker.sock:/var/run/docker.sock
+      - ./traefik.toml:/traefik.toml
+      - ./ssl/test.key:/ssl/traefik.key
+      - ./ssl/test.crt:/ssl/traefik.crt
+    ports:
+      - '80:80'
+      - '443:443'
+      - '8040:8040'
+    networks:
+      federation:
+
+
+networks:
+  federation:
+    external:
+      name: federation
diff --git a/docs/features.rst b/docs/features.rst
index d72a826b68992faad0666df89f6362afb4997ab8..e0f490e4b741676085e2bc6cbeae01c8e5d19f85 100644
--- a/docs/features.rst
+++ b/docs/features.rst
@@ -42,3 +42,14 @@ The project itself is splitted in two parts:
 While the main interface to the server and API is the bundled front-end, the project itself is agnostic in the way you connect to it. Therefore, desktop clients or apps could be developped and implement the same (or even more) features as the bundled frontend.
 
 This modularity also makes it possible do deploy only a single component from the system.
+
+Federation
+----------
+
+Each Funkwhale instance is able to fetch music from other compatible servers,
+and share its own library on the network. The federation is implemented
+using the ActivityPub protocol, in order to leverage existing tools
+and be compatible with other services such as Mastodon.
+
+As of today, federation only targets music acquisition, meaning user interaction
+are not shared via ActivityPub. This will be implemented at a later point.
diff --git a/docs/federation.rst b/docs/federation.rst
new file mode 100644
index 0000000000000000000000000000000000000000..5b030074c04eb7e4a47c3372514e5482e649b437
--- /dev/null
+++ b/docs/federation.rst
@@ -0,0 +1,57 @@
+Federation
+==========
+
+Each Funkwale instance can federates its music library with other instances
+of the network. This means that an instance A can acquire music from instance B
+and share its own library with an instance C.
+
+We support various levels of controls for federation-related features.
+
+Managing federation
+-------------------
+
+Federation management is only available to instance admins and users
+who have the proper permissions. You can disable federation completely
+at the instance level by setting the FEDERATION_ENABLED environment variable
+to False.
+
+On the front end, assuming you have the proper permission, you will see
+a "Federation" link in the sidebar.
+
+
+Acquire music via federation
+----------------------------
+
+Instance libraries are protected by default. To access another instance
+library, you have to follow it. Each funkwhale instance gets a dedicated
+ActivityPub Actor you can follow via the username "library@yourinstance.domain".
+
+When submitted, a follow request will be sent to
+the other instance which can accept or deny it. Once your follow request
+is accepted, you can start browsing the other instance library
+and import music from it.
+
+By default, we do not duplicate audio files from federated tracks, to reduce
+disk usage on your instance. When someone listens to a federated track,
+the audio file is requested on the fly from the remote instance, and
+store in a local cache. It is automatically deleted after a configurable
+amount of time if it was not listened again in the meantime.
+
+If you want to mirror a remote instance collection, including its audio files,
+we offer an option for that.
+
+We also support an "autoimport" mode for each remote library. When enabled,
+any new track published in the remote library will be directly imported
+in your instance.
+
+Share music via federation
+--------------------------
+
+Federation is enabled by default, but requires manually approving
+each other instance asking for access to library. This is by design,
+to ensure your library is not shared publicly without your consent.
+
+However, if you're confident about federating publicly without manual approval,
+you can set the FEDERATION_MUSIC_NEEDS_APPROVAL environment variable to false.
+Follow requests will be accepted automatically and followers
+given access to your library without manual intervention.
diff --git a/docs/importing-music.rst b/docs/importing-music.rst
index 92ff6bfb503208319ecef08347fdf72cf8d7709f..f09eea7b184d86bd465f5ba40c4c49343066da31 100644
--- a/docs/importing-music.rst
+++ b/docs/importing-music.rst
@@ -67,3 +67,9 @@ under creative commons (courtesy of Jamendo):
     ./download-tracks.sh music.txt
 
 This will download a bunch of zip archives (one per album) under the ``data/music`` directory and unzip their content.
+
+From other instances
+--------------------
+
+Funkwhale also supports importing music from other instances. Please refer
+to :doc:`federation` for more details.
diff --git a/docs/index.rst b/docs/index.rst
index f5acecce516ee8d6b635c91e3aa0fed2686f5739..82dcf8c88d475ddf53cea25133ca80c4c4feae2d 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -15,6 +15,7 @@ Funkwhale is a self-hosted, modern free and open-source music server, heavily in
    installation/index
    configuration
    importing-music
+   federation
    upgrading
    changelog
 
diff --git a/docs/installation/systemd.rst b/docs/installation/systemd.rst
index 67af98432641dc837c8313ca1578cc3974dee70f..27781c447291cfcbc0ab9279bf3eb801336ff704 100644
--- a/docs/installation/systemd.rst
+++ b/docs/installation/systemd.rst
@@ -13,11 +13,13 @@ First, download the sample unitfiles:
     curl -L -o "/etc/systemd/system/funkwhale.target" "https://code.eliotberriot.com/funkwhale/funkwhale/raw/|version|/deploy/funkwhale.target"
     curl -L -o "/etc/systemd/system/funkwhale-server.service" "https://code.eliotberriot.com/funkwhale/funkwhale/raw/|version|/deploy/funkwhale-server.service"
     curl -L -o "/etc/systemd/system/funkwhale-worker.service" "https://code.eliotberriot.com/funkwhale/funkwhale/raw/|version|/deploy/funkwhale-worker.service"
+    curl -L -o "/etc/systemd/system/funkwhale-beat.service" "https://code.eliotberriot.com/funkwhale/funkwhale/raw/|version|/deploy/funkwhale-beat.service"
 
 This will download three unitfiles:
 
 - ``funkwhale-server.service`` to launch the funkwhale web server
 - ``funkwhale-worker.service`` to launch the funkwhale task worker
+- ``funkwhale-beat.service`` to launch the funkwhale task beat (this is for recurring tasks)
 - ``funkwhale.target`` to easily stop and start all of the services at once
 
 You can of course review and edit them to suit your deployment scenario
diff --git a/front/.dockerignore b/front/.dockerignore
new file mode 100644
index 0000000000000000000000000000000000000000..3c3629e647f5ddf82548912e337bea9826b434af
--- /dev/null
+++ b/front/.dockerignore
@@ -0,0 +1 @@
+node_modules
diff --git a/front/Dockerfile b/front/Dockerfile
index 60b03c9b8e3c9ff54cf7dee5f7e4e96273a749b6..3d4c65e6418514a3db595bce3896655edaac82a7 100644
--- a/front/Dockerfile
+++ b/front/Dockerfile
@@ -4,7 +4,7 @@ EXPOSE 8080
 WORKDIR /app/
 ADD package.json .
 RUN yarn install
-VOLUME ["/app/node_modules"]
+
 COPY . .
 
 CMD ["npm", "run", "dev"]
diff --git a/front/build/dev-server.js b/front/build/dev-server.js
index 634a6d41e76af052331174198af1fd5e0669fd06..f9c389e729867b266904026454b5c676ea0df1da 100644
--- a/front/build/dev-server.js
+++ b/front/build/dev-server.js
@@ -14,6 +14,8 @@ var webpackConfig = process.env.NODE_ENV === 'testing'
   ? require('./webpack.prod.conf')
   : require('./webpack.dev.conf')
 
+require('./i18n')
+
 // default port where dev server listens for incoming traffic
 var port = process.env.PORT || config.dev.port
 var host = process.env.HOST || config.dev.host
diff --git a/front/build/i18n.js b/front/build/i18n.js
new file mode 100644
index 0000000000000000000000000000000000000000..ef31070c7d913acd8ebe769ac1a10a6faf558288
--- /dev/null
+++ b/front/build/i18n.js
@@ -0,0 +1,49 @@
+const fs = require('fs');
+const path = require('path');
+const { gettextToI18next } = require('i18next-conv');
+
+const poDir = path.join(__dirname, '..', '..', 'po')
+const outDir = path.join(__dirname, '..', 'static', 'translations')
+if (!fs.existsSync(outDir) || !fs.statSync(outDir).isDirectory()) {
+    fs.mkdirSync(outDir)
+}
+
+// Convert .po files to i18next files
+fs.readdir(poDir, (err, files) => {
+    if (err) {
+        return console.log(err)
+    }
+
+    for (const file of files) {
+        if (file.endsWith('.po')) {
+            const lang = file.replace(/\.po$/, '')
+            const output = path.join(outDir, `${lang}.json`)
+            fs.readFile(path.join(poDir, file), (err, content) => {
+                if (err) {
+                    return console.log(err)
+                }
+
+                gettextToI18next(lang, content).then(res => {
+                    fs.writeFile(output, res, err => {
+                        if (err) {
+                            console.log(err)
+                        } else {
+                            console.log(`Wrote translation file: ${output}`)
+                            if (lang === 'en') {
+                                // for english, we need to specify that json values are equal to the keys.
+                                // otherwise we end up with empty strings on the front end for english
+                                var contents = fs.readFileSync(output)
+                                var jsonContent = JSON.parse(contents)
+                                var finalContent = {}
+                                Object.keys(jsonContent).forEach(function(key) {
+                                    finalContent[key] = key
+                                })
+                                fs.writeFile(output, JSON.stringify(finalContent))
+                            }
+                        }
+                    })
+                })
+            })
+        }
+    }
+})
diff --git a/front/package.json b/front/package.json
index d67375f7e5851f7cf2af379ac298a6f85f973f78..8844e8bee53c05c4436ec6e8783ca405972162ae 100644
--- a/front/package.json
+++ b/front/package.json
@@ -8,6 +8,8 @@
     "dev": "node build/dev-server.js",
     "start": "node build/dev-server.js",
     "build": "node build/build.js",
+    "i18n-extract": "find src/ -name '*.vue' | xargs vendor/vue-i18n-xgettext/index.js > ../po/en.po",
+    "i18n-compile": "node build/i18n.js",
     "unit": "cross-env BABEL_ENV=test karma start test/unit/karma.conf.js --single-run",
     "unit-watch": "cross-env BABEL_ENV=test karma start test/unit/karma.conf.js",
     "e2e": "node test/e2e/runner.js",
@@ -15,9 +17,13 @@
     "lint": "eslint --ext .js,.vue src test/unit/specs test/e2e/specs"
   },
   "dependencies": {
+    "@panter/vue-i18next": "^0.9.1",
     "axios": "^0.17.1",
     "dateformat": "^2.0.0",
     "django-channels": "^1.1.6",
+    "i18next": "^11.1.1",
+    "i18next-conv": "^6.0.0",
+    "i18next-fetch-backend": "^0.1.0",
     "js-logger": "^1.3.0",
     "jwt-decode": "^2.2.0",
     "lodash": "^4.17.4",
@@ -34,7 +40,7 @@
     "vue-upload-component": "^2.7.4",
     "vuedraggable": "^2.14.1",
     "vuex": "^3.0.1",
-    "vuex-persistedstate": "^2.4.2",
+    "vuex-persistedstate": "^2.5.2",
     "vuex-router-sync": "^5.0.0"
   },
   "devDependencies": {
@@ -98,6 +104,7 @@
     "sinon-chai": "^2.8.0",
     "sinon-stub-promise": "^4.0.0",
     "url-loader": "^0.5.8",
+    "vue-i18n-xgettext": "^0.0.4",
     "vue-loader": "^12.1.0",
     "vue-style-loader": "^3.0.1",
     "vue-template-compiler": "^2.3.3",
diff --git a/front/src/App.vue b/front/src/App.vue
index e8ab18694ae4c44a5c6995d263b194dffe598dc2..e8cac74761c84e5d26ea354fb400b61f3543e0f4 100644
--- a/front/src/App.vue
+++ b/front/src/App.vue
@@ -7,21 +7,25 @@
       <div class="ui container">
         <div class="ui stackable equal height stackable grid">
           <div class="three wide column">
-            <h4 class="ui header">Links</h4>
+            <i18next tag="h4" class="ui header" path="Links"></i18next>
             <div class="ui link list">
               <router-link class="item" to="/about">
-                About this instance
+                <i18next path="About this instance" />
               </router-link>
-              <a href="https://funkwhale.audio" class="item" target="_blank">Official website</a>
-              <a href="https://docs.funkwhale.audio" class="item" target="_blank">Documentation</a>
-              <a href="https://code.eliotberriot.com/funkwhale/funkwhale" class="item" target="_blank">Source code</a>
-              <a href="https://code.eliotberriot.com/funkwhale/funkwhale/issues" class="item" target="_blank">Issue tracker</a>
+              <i18next tag="a" href="https://funkwhale.audio" class="item" target="_blank" path="Official website" />
+              <i18next tag="a" href="https://docs.funkwhale.audio" class="item" target="_blank" path="Documentation" />
+              <i18next tag="a" href="https://code.eliotberriot.com/funkwhale/funkwhale" class="item" target="_blank" path="Source code" />
+              <i18next tag="a" href="https://code.eliotberriot.com/funkwhale/funkwhale/issues" class="item" target="_blank" path="Issue tracker" />
             </div>
           </div>
           <div class="ten wide column">
-            <h4 class="ui header">About funkwhale</h4>
-            <p>Funkwhale is a free and open-source project run by volunteers. You can help us improve the platform by reporting bugs, suggesting features and share the project with your friends!</p>
-            <p>The funkwhale logo was kindly designed and provided by Francis Gading.</p>
+            <i18next tag="h4" class="ui header" path="About funkwhale" />
+            <p>
+              <i18next path="Funkwhale is a free and open-source project run by volunteers. You can help us improve the platform by reporting bugs, suggesting features and share the project with your friends!"/>
+            </p>
+            <p>
+              <i18next path="The funkwhale logo was kindly designed and provided by Francis Gading."/>
+            </p>
           </div>
         </div>
       </div>
@@ -31,7 +35,6 @@
       :dsn="$store.state.instance.settings.raven.front_dsn.value">
     </raven>
     <playlist-modal v-if="$store.state.auth.authenticated"></playlist-modal>
-
   </div>
 </template>
 
diff --git a/front/src/components/Home.vue b/front/src/components/Home.vue
index 0e24dcd59008c52fe198e6a1778d9d1e0c00545c..ce1307ff0c8a84b934f027c7a27509644edc32a5 100644
--- a/front/src/components/Home.vue
+++ b/front/src/components/Home.vue
@@ -3,7 +3,7 @@
     <div class="ui vertical center aligned stripe segment">
       <div class="ui text container">
         <h1 class="ui huge header">
-            Welcome on Funkwhale
+          Welcome on Funkwhale
         </h1>
         <p>We think listening music should be simple.</p>
         <router-link class="ui icon button" to="/about">
diff --git a/front/src/components/Pagination.vue b/front/src/components/Pagination.vue
index 47cf5183ab31aeb6fa4575139d42db73fc53a550..ca40cdd0762862e8371cdd2f6928dff25a0da0f8 100644
--- a/front/src/components/Pagination.vue
+++ b/front/src/components/Pagination.vue
@@ -1,25 +1,25 @@
 <template>
-  <div class="ui pagination borderless menu">
-    <a
-      v-if="current - 1 >= 1"
+  <div class="ui pagination menu">
+    <div
+      :disabled="current - 1 < 1"
       @click="selectPage(current - 1)"
-      :class="[{'disabled': current - 1 < 1}, 'item']"><i class="angle left icon"></i></a>
-    <template>
-      <a
+      :class="[{'disabled': current - 1 < 1}, 'item']"><i class="angle left icon"></i></div>
+    <template v-if="!compact">
+      <div
         v-if="page !== 'skip'"
         v-for="page in pages"
         @click="selectPage(page)"
         :class="[{'active': page === current}, 'item']">
         {{ page }}
-      </a>
-      <a v-else class="disabled item">
+      </div>
+      <div v-else class="disabled item">
         ...
-      </a>
+      </div>
     </template>
-    <a
-      v-if="current + 1 <= maxPage"
+    <div
+      :disabled="current + 1 > maxPage"
       @click="selectPage(current + 1)"
-      :class="[{'disabled': current + 1 > maxPage}, 'item']"><i class="angle right icon"></i></a>
+      :class="[{'disabled': current + 1 > maxPage}, 'item']"><i class="angle right icon"></i></div>
   </div>
 </template>
 
@@ -30,7 +30,8 @@ export default {
   props: {
     current: {type: Number, default: 1},
     paginateBy: {type: Number, default: 25},
-    total: {type: Number}
+    total: {type: Number},
+    compact: {type: Boolean, default: false}
   },
   computed: {
     pages: function () {
@@ -72,6 +73,9 @@ export default {
   },
   methods: {
     selectPage: function (page) {
+      if (page > this.maxPage || page < 1) {
+        return
+      }
       if (this.current !== page) {
         this.$emit('page-changed', page)
       }
diff --git a/front/src/components/Sidebar.vue b/front/src/components/Sidebar.vue
index 42a923b6b3faaf5586a35613277bbcdd7ea8c27d..96047ab9848621c55fe5b75971bfca47e6c9ba62 100644
--- a/front/src/components/Sidebar.vue
+++ b/front/src/components/Sidebar.vue
@@ -45,6 +45,9 @@
         <router-link
           v-if="$store.state.auth.authenticated"
           class="item" :to="{path: '/activity'}"><i class="bell icon"></i> Activity</router-link>
+        <router-link
+          class="item" v-if="$store.state.auth.availablePermissions['federation.manage']"
+          :to="{path: '/manage/federation/libraries'}"><i class="sitemap icon"></i> Federation</router-link>
       </div>
 
       <player></player>
diff --git a/front/src/components/activity/Like.vue b/front/src/components/activity/Like.vue
index ffb8312787fa366490e0172a0f71dcdbffbfc77d..deda121cc9f8def680eeebd08159fc92c5ee97db 100644
--- a/front/src/components/activity/Like.vue
+++ b/front/src/components/activity/Like.vue
@@ -5,17 +5,20 @@
    </div>
    <div class="content">
      <div class="summary">
-       <slot name="user"></slot>
-       favorited a track
-       <slot name="date"></slot>
+       <i18next path="{%0%} favorited a track {%1%}">
+        <slot name="user"></slot>
+        <slot name="date"></slot>
+       </i18next>
      </div>
      <div class="extra text">
        <router-link :to="{name: 'library.tracks.detail', params: {id: event.object.local_id }}">{{ event.object.name }}</router-link>
-        <template v-if="event.object.album">from album {{ event.object.album }}, by <em>{{ event.object.artist }}</em>
-        </template>
-        <template v-else>, by <em>{{ event.object.artist }}</em>
-        </template>
-
+        <i18next path="from album {%0%}, by {%1%}" v-if="event.object.album">
+          {{ event.object.album }}
+          <em>{{ event.object.artist }}</em>
+        </i18next>
+        <i18next path=", by {%0%}" v-else>
+          <em>{{ event.object.artist }}</em>
+        </i18next>
       </div>
    </div>
  </div>
diff --git a/front/src/components/activity/Listen.vue b/front/src/components/activity/Listen.vue
index 7c8ee8a69e30666b7fc09e1264e75e6b49ade867..d207c280deef14f44cada9342a2732eabfdc1c48 100644
--- a/front/src/components/activity/Listen.vue
+++ b/front/src/components/activity/Listen.vue
@@ -5,17 +5,20 @@
    </div>
    <div class="content">
      <div class="summary">
-       <slot name="user"></slot>
-       listened to a track
-       <slot name="date"></slot>
+       <i18next path="{%0%} listened to a track {%1%}">
+        <slot name="user"></slot>
+        <slot name="date"></slot>
+       </i18next>
      </div>
      <div class="extra text">
        <router-link :to="{name: 'library.tracks.detail', params: {id: event.object.local_id }}">{{ event.object.name }}</router-link>
-        <template v-if="event.object.album">from album {{ event.object.album }}, by <em>{{ event.object.artist }}</em>
-        </template>
-        <template v-else>, by <em>{{ event.object.artist }}</em>
-        </template>
-
+        <i18next path="from album {%0%}, by {%1%}" v-if="event.object.album">
+          {{ event.object.album }}
+          <em>{{ event.object.artist }}</em>
+        </i18next>
+        <i18next path=", by {%0%}" v-else>
+          <em>{{ event.object.artist }}</em>
+        </i18next>
       </div>
    </div>
  </div>
diff --git a/front/src/components/audio/PlayButton.vue b/front/src/components/audio/PlayButton.vue
index f2a3898622c09852eb6e05b77c549a76bbf9a6ee..14d381ca19517efa6641e047fc94cd6aed4f1795 100644
--- a/front/src/components/audio/PlayButton.vue
+++ b/front/src/components/audio/PlayButton.vue
@@ -1,18 +1,18 @@
 <template>
   <div :class="['ui', {'tiny': discrete}, 'buttons']">
     <button
-      title="Add to current queue"
+      :title="$t('Add to current queue')"
       @click="add"
       :class="['ui', {loading: isLoading}, {'mini': discrete}, {disabled: !playable}, 'button']">
       <i class="ui play icon"></i>
-      <template v-if="!discrete"><slot>Play</slot></template>
+      <template v-if="!discrete"><slot><i18next path="Play"/></slot></template>
     </button>
     <div v-if="!discrete" :class="['ui', {disabled: !playable}, 'floating', 'dropdown', 'icon', 'button']">
       <i class="dropdown icon"></i>
       <div class="menu">
-        <div class="item"@click="add"><i class="plus icon"></i> Add to queue</div>
-        <div class="item"@click="addNext()"><i class="step forward icon"></i> Play next</div>
-        <div class="item"@click="addNext(true)"><i class="arrow down icon"></i> Play now</div>
+        <div class="item"@click="add"><i class="plus icon"></i><i18next path="Add to queue"/></div>
+        <div class="item"@click="addNext()"><i class="step forward icon"></i><i18next path="Play next"/></div>
+        <div class="item"@click="addNext(true)"><i class="arrow down icon"></i><i18next path="Play now"/></div>
       </div>
     </div>
   </div>
diff --git a/front/src/components/audio/Player.vue b/front/src/components/audio/Player.vue
index 31f6dc35ac3478a9b3c84b15a76ea9adcc044882..ad90a599528a09cfd287389ddfe22a7585f9fa69 100644
--- a/front/src/components/audio/Player.vue
+++ b/front/src/components/audio/Player.vue
@@ -57,44 +57,44 @@
 
       <div class="two wide column controls ui grid">
         <div
-          title="Previous track"
+          :title="$t('Previous track')"
           class="two wide column control"
           :disabled="emptyQueue">
             <i @click="previous" :class="['ui', 'backward', {'disabled': emptyQueue}, 'big', 'icon']"></i>
         </div>
         <div
           v-if="!playing"
-          title="Play track"
+          :title="$t('Play track')"
           class="two wide column control">
             <i @click="togglePlay" :class="['ui', 'play', {'disabled': !currentTrack}, 'big', 'icon']"></i>
         </div>
         <div
           v-else
-          title="Pause track"
+          :title="$t('Pause track')"
           class="two wide column control">
             <i @click="togglePlay" :class="['ui', 'pause', {'disabled': !currentTrack}, 'big', 'icon']"></i>
         </div>
         <div
-          title="Next track"
+          :title="$t('Next track')"
           class="two wide column control"
           :disabled="!hasNext">
             <i @click="next" :class="['ui', {'disabled': !hasNext}, 'step', 'forward', 'big', 'icon']" ></i>
         </div>
         <div class="two wide column control volume-control">
-          <i title="Unmute" @click="$store.commit('player/volume', 1)" v-if="volume === 0" class="volume off secondary icon"></i>
-          <i title="Mute" @click="$store.commit('player/volume', 0)" v-else-if="volume < 0.5" class="volume down secondary icon"></i>
-          <i title="Mute" @click="$store.commit('player/volume', 0)" v-else class="volume up secondary icon"></i>
+          <i :title="$t('Unmute')" @click="$store.commit('player/volume', 1)" v-if="volume === 0" class="volume off secondary icon"></i>
+          <i :title="$t('Mute')" @click="$store.commit('player/volume', 0)" v-else-if="volume < 0.5" class="volume down secondary icon"></i>
+          <i :title="$t('Mute')" @click="$store.commit('player/volume', 0)" v-else class="volume up secondary icon"></i>
           <input type="range" step="0.05" min="0" max="1" v-model="sliderVolume" />
         </div>
         <div class="two wide column control looping">
           <i
-            title="Looping disabled. Click to switch to single-track looping."
+            :title="$t('Looping disabled. Click to switch to single-track looping.')"
             v-if="looping === 0"
             @click="$store.commit('player/looping', 1)"
             :disabled="!currentTrack"
             :class="['ui', {'disabled': !currentTrack}, 'step', 'repeat', 'secondary', 'icon']"></i>
           <i
-            title="Looping on a single track. Click to switch to whole queue looping."
+            :title="$t('Looping on a single track. Click to switch to whole queue looping.')"
             v-if="looping === 1"
             @click="$store.commit('player/looping', 2)"
             :disabled="!currentTrack"
@@ -102,7 +102,7 @@
             <span class="ui circular tiny orange label">1</span>
           </i>
           <i
-            title="Looping on whole queue. Click to disable looping."
+            :title="$t('Looping on whole queue. Click to disable looping.')"
             v-if="looping === 2"
             @click="$store.commit('player/looping', 0)"
             :disabled="!currentTrack"
@@ -111,14 +111,14 @@
         </div>
         <div
           :disabled="queue.tracks.length === 0"
-          title="Shuffle your queue"
+          :title="$t('Shuffle your queue')"
           class="two wide column control">
           <i @click="shuffle()" :class="['ui', 'random', 'secondary', {'disabled': queue.tracks.length === 0}, 'icon']" ></i>
         </div>
         <div class="one wide column"></div>
         <div
           :disabled="queue.tracks.length === 0"
-          title="Clear your queue"
+          :title="$t('Clear your queue')"
           class="two wide column control">
           <i @click="clean()" :class="['ui', 'trash', 'secondary', {'disabled': queue.tracks.length === 0}, 'icon']" ></i>
         </div>
diff --git a/front/src/components/audio/Search.vue b/front/src/components/audio/Search.vue
index bb0881862397324f679296ae5cf6dfd93869cea6..890c83f5bfc2d6562eedfda26b9a63a4fe631bf1 100644
--- a/front/src/components/audio/Search.vue
+++ b/front/src/components/audio/Search.vue
@@ -1,6 +1,6 @@
 <template>
   <div>
-    <h2>Search for some music</h2>
+    <h2><i18next path="Search for some music"/></h2>
     <div :class="['ui', {'loading': isLoading }, 'search']">
       <div class="ui icon big input">
         <i class="search icon"></i>
@@ -8,22 +8,22 @@
       </div>
     </div>
     <template v-if="query.length > 0">
-      <h3 class="ui title">Artists</h3>
+      <h3 class="ui title"><i18next path="Artists"/></h3>
       <div v-if="results.artists.length > 0" class="ui stackable three column grid">
         <div class="column" :key="artist.id" v-for="artist in results.artists">
           <artist-card class="fluid" :artist="artist" ></artist-card>
         </div>
       </div>
-      <p v-else>Sorry, we did not found any artist matching your query</p>
+      <p v-else><i18next path="Sorry, we did not found any artist matching your query"/></p>
     </template>
     <template v-if="query.length > 0">
-      <h3 class="ui title">Albums</h3>
+      <h3 class="ui title"><i18next path="Albums"/></h3>
       <div v-if="results.albums.length > 0" class="ui stackable three column grid">
         <div class="column" :key="album.id" v-for="album in results.albums">
           <album-card class="fluid" :album="album" ></album-card>
         </div>
       </div>
-      <p v-else>Sorry, we did not found any album matching your query</p>
+      <p v-else><i18next path="Sorry, we did not found any album matching your query"/></p>
     </template>
   </div>
 </template>
diff --git a/front/src/components/audio/album/Card.vue b/front/src/components/audio/album/Card.vue
index bb37b2b2f86635e3324652f4245b08e3fa4a26a9..6742dca4f9b94e88983ad6ec6d4396b11dc14648 100644
--- a/front/src/components/audio/album/Card.vue
+++ b/front/src/components/audio/album/Card.vue
@@ -10,8 +10,10 @@
         </div>
         <div class="meta">
           <span>
-            By <router-link tag="span" :to="{name: 'library.artists.detail', params: {id: album.artist.id }}">
+            <i18next path="By {%0%}">
+              <router-link tag="span" :to="{name: 'library.artists.detail', params: {id: album.artist.id }}">
               {{ album.artist.name }}</router-link>
+            </i18next>
           </span><span class="time" v-if="album.release_date">– {{ album.release_date | year }}</span>
         </div>
         <div class="description" v-if="mode === 'rich'">
@@ -36,16 +38,24 @@
             </tbody>
           </table>
           <div class="center aligned segment" v-if="album.tracks.length > initialTracks">
-            <em v-if="!showAllTracks" @click="showAllTracks = true" class="expand">Show {{ album.tracks.length - initialTracks }} more tracks</em>
-            <em v-else @click="showAllTracks = false" class="expand">Collapse</em>
+            <em v-if="!showAllTracks" @click="showAllTracks = true" class="expand">
+              <i18next path="Show {%0%} more tracks">{{ album.tracks.length - initialTracks }}</i18next>
+            </em>
+            <em v-else @click="showAllTracks = false" class="expand">
+              <i18next path="Collapse" />
+            </em>
           </div>
         </div>
       </div>
       <div class="extra content">
-        <play-button class="mini basic orange right floated" :tracks="album.tracks">Play all</play-button>
+        <play-button class="mini basic orange right floated" :tracks="album.tracks">
+          <i18next path="Play all"/>
+        </play-button>
         <span>
           <i class="music icon"></i>
-          {{ album.tracks.length }} tracks
+          <i18next path="{%0%} tracks">
+            {{ album.tracks.length }}
+          </i18next>
         </span>
       </div>
     </div>
diff --git a/front/src/components/audio/artist/Card.vue b/front/src/components/audio/artist/Card.vue
index a5111434546e43c233d717616aa4363c7e6a36f3..3ad6fb1c60263ac2d350350b3318eecec72885d0 100644
--- a/front/src/components/audio/artist/Card.vue
+++ b/front/src/components/audio/artist/Card.vue
@@ -27,17 +27,27 @@
             </tbody>
           </table>
           <div class="center aligned segment" v-if="artist.albums.length > initialAlbums">
-            <em v-if="!showAllAlbums" @click="showAllAlbums = true" class="expand">Show {{ artist.albums.length - initialAlbums }} more albums</em>
-            <em v-else @click="showAllAlbums = false" class="expand">Collapse</em>
+            <em v-if="!showAllAlbums" @click="showAllAlbums = true" class="expand">
+              <i18next path="Show {%0%} more albums">
+                {{ artist.albums.length - initialAlbums }}
+              </i18next>
+            </em>
+            <em v-else @click="showAllAlbums = false" class="expand">
+              <i18next path="Collapse"/>
+            </em>
           </div>
         </div>
     </div>
     <div class="extra content">
         <span>
           <i class="sound icon"></i>
-          {{ artist.albums.length }} albums
+          <i18next path="{%0%} albums">
+            {{ artist.albums.length }}
+          </i18next>
         </span>
-        <play-button class="mini basic orange right floated" :tracks="allTracks">Play all</play-button>
+        <play-button class="mini basic orange right floated" :tracks="allTracks">
+          <i18next path="Play all"/>
+        </play-button>
       </div>
     </div>
 </template>
diff --git a/front/src/components/audio/track/Table.vue b/front/src/components/audio/track/Table.vue
index 512ba1b493d35f71b098d112959a9ac9d3c5e4ef..7045cf9bd222085bbb7fbf1056a24f4c8205cb86 100644
--- a/front/src/components/audio/track/Table.vue
+++ b/front/src/components/audio/track/Table.vue
@@ -4,9 +4,9 @@
       <tr>
         <th></th>
         <th></th>
-        <th colspan="6">Title</th>
-        <th colspan="6">Artist</th>
-        <th colspan="6">Album</th>
+        <i18next tag="th" colspan="6" path="Title"/>
+        <i18next tag="th" colspan="6" path="Artist"/>
+        <i18next tag="th" colspan="6" path="Album"/>
         <th></th>
       </tr>
     </thead>
@@ -20,20 +20,18 @@
     <tfoot class="full-width">
       <tr>
         <th colspan="3">
-          <button @click="showDownloadModal = !showDownloadModal" class="ui basic button">Download...</button>
+          <button @click="showDownloadModal = !showDownloadModal" class="ui basic button">
+            <i18next path="Download..."/>
+          </button>
           <modal :show.sync="showDownloadModal">
-            <div class="header">
-              Download tracks
-            </div>
+            <i18next tag="div" path="Download tracks" class="header" />
             <div class="content">
               <div class="description">
-                <p>There is currently no way to download directly multiple tracks from funkwhale as a ZIP archive.
-                  However, you can use a command line tools such as <a href="https://curl.haxx.se/" target="_blank">cURL</a> to easily download a list of tracks.
-                </p>
-                <p>Simply copy paste the snippet below into a terminal to launch the download.</p>
-                <div class="ui warning message">
-                  Keep your PRIVATE_TOKEN secret as it gives access to your account.
-                </div>
+                <i18next tag="p" path="There is currently no way to download directly multiple tracks from funkwhale as a ZIP archive. However, you can use a command line tools such as {%0%} to easily download a list of tracks.">
+                  <a href="https://curl.haxx.se/" target="_blank">cURL</a>
+                </i18next>
+                <i18next path="Simply copy paste the snippet below into a terminal to launch the download."/>
+                <i18next tag="div" class="ui warning message" path="Keep your PRIVATE_TOKEN secret as it gives access to your account."/>
                 <pre>
 export PRIVATE_TOKEN="{{ $store.state.auth.token }}"
 <template v-for="track in tracks"><template v-if="track.files.length > 0">
@@ -42,9 +40,7 @@ curl -G -o "{{ track.files[0].filename }}" <template v-if="$store.state.auth.aut
               </div>
             </div>
             <div class="actions">
-              <div class="ui black deny button">
-                Cancel
-              </div>
+              <i18next tag="div" class="ui black deny button" path="Cancel" />
             </div>
           </modal>
         </th>
diff --git a/front/src/components/auth/Login.vue b/front/src/components/auth/Login.vue
index d232da2a35e38d4f774072fb049473ac5cfa1e86..b06ce89f0da850457c10acaee1d894df98368750 100644
--- a/front/src/components/auth/Login.vue
+++ b/front/src/components/auth/Login.vue
@@ -2,17 +2,17 @@
   <div class="main pusher" v-title="'Log In'">
     <div class="ui vertical stripe segment">
       <div class="ui small text container">
-        <h2>Log in to your Funkwhale account</h2>
+        <h2><i18next path="Log in to your Funkwhale account"/></h2>
         <form class="ui form" @submit.prevent="submit()">
           <div v-if="error" class="ui negative message">
-            <div class="header">We cannot log you in</div>
+            <div class="header"><i18next path="We cannot log you in"/></div>
             <ul class="list">
-              <li v-if="error == 'invalid_credentials'">Please double-check your username/password couple is correct</li>
-              <li v-else>An unknown error happend, this can mean the server is down or cannot be reached</li>
+              <i18next tag="li" v-if="error == 'invalid_credentials'" path="Please double-check your username/password couple is correct"/>
+              <i18next tag="li" v-else path="An unknown error happend, this can mean the server is down or cannot be reached"/>
             </ul>
           </div>
           <div class="field">
-            <label>Username or email</label>
+            <i18next tag="label" path="Username or email"/>
             <input
             ref="username"
             required
@@ -23,7 +23,7 @@
             >
           </div>
           <div class="field">
-            <label>Password</label>
+            <i18next tag="label" path="Password"/>            
             <input
             required
             type="password"
@@ -31,9 +31,9 @@
             v-model="credentials.password"
             >
           </div>
-          <button :class="['ui', {'loading': isLoading}, 'button']" type="submit">Login</button>
+          <button :class="['ui', {'loading': isLoading}, 'button']" type="submit"><i18next path="Login"/></button>
           <router-link class="ui right floated basic button" :to="{path: '/signup'}">
-            Create an account
+            <i18next path="Create an account"/>
           </router-link>
         </form>
       </div>
diff --git a/front/src/components/auth/Logout.vue b/front/src/components/auth/Logout.vue
index 980740c94cfc415e5afc467186c14d1a193748cc..f5a994e73ff71d96a77fb5bf2fa1b96da8ec7466 100644
--- a/front/src/components/auth/Logout.vue
+++ b/front/src/components/auth/Logout.vue
@@ -2,9 +2,9 @@
   <div class="main pusher" v-title="'Log Out'">
     <div class="ui vertical stripe segment">
       <div class="ui small text container">
-        <h2>Are you sure you want to log out?</h2>
-        <p>You are currently logged in as {{ $store.state.auth.username }}</p>
-        <button class="ui button" @click="$store.dispatch('auth/logout')">Yes, log me out!</button>
+        <h2><i18next path="Are you sure you want to log out?"/></h2>
+        <i18next tag="p" path="You are currently logged in as {%0%}">{{ $store.state.auth.username }}</i18next>
+        <button class="ui button" @click="$store.dispatch('auth/logout')"><i18next path="Yes, log me out!"/></button>
         </form>
       </div>
     </div>
diff --git a/front/src/components/auth/Profile.vue b/front/src/components/auth/Profile.vue
index 9fafccffa83f0e9b63ac6a692de586a4adf9e4f8..ab728c036beb8a29518bb1851a172ff38c1f62d0 100644
--- a/front/src/components/auth/Profile.vue
+++ b/front/src/components/auth/Profile.vue
@@ -9,16 +9,17 @@
           <i class="circular inverted user green icon"></i>
           <div class="content">
             {{ $store.state.auth.profile.username }}
-            <div class="sub header">Registered since {{ signupDate }}</div>
+            <i18next class="sub header" path="Registered since {%0%}">{{ signupDate }}</i18next>
           </div>
         </h2>
-        <div class="ui basic green label">this is you!</div>
+        <div class="ui basic green label"><i18next path="This is you!"/></div>
         <div v-if="$store.state.auth.profile.is_staff" class="ui yellow label">
           <i class="star icon"></i>
-          Staff member
+          <i18next path="Staff member"/>
         </div>
         <router-link class="ui tiny basic button" :to="{path: '/settings'}">
-          <i class="setting icon"> </i>Settings...
+          <i class="setting icon"> </i>
+          <i18next path="Settings..."/>
         </router-link>
 
       </div>
diff --git a/front/src/components/auth/Settings.vue b/front/src/components/auth/Settings.vue
index 859c0d04b97ccecb97911575ad505cdda475ebf5..c847bde888efb2a0b3bca619dd5cc17e6c3b62d4 100644
--- a/front/src/components/auth/Settings.vue
+++ b/front/src/components/auth/Settings.vue
@@ -2,13 +2,13 @@
   <div class="main pusher" v-title="'Account Settings'">
     <div class="ui vertical stripe segment">
       <div class="ui small text container">
-        <h2 class="ui header">Account settings</h2>
+        <h2 class="ui header"><i18next path="Account settings"/></h2>
         <form class="ui form" @submit.prevent="submitSettings()">
           <div v-if="settings.success" class="ui positive message">
-            <div class="header">Settings updated</div>
+            <div class="header"><i18next path="Settings updated"/></div>
           </div>
           <div v-if="settings.errors.length > 0" class="ui negative message">
-            <div class="header">We cannot save your settings</div>
+            <i18next tag="div" class="header" path="We cannot save your settings"/>
             <ul class="list">
               <li v-for="error in settings.errors">{{ error }}</li>
             </ul>
@@ -20,21 +20,21 @@
               <option :value="c.value" v-for="c in f.choices">{{ c.label }}</option>
             </select>
           </div>
-          <button :class="['ui', {'loading': isLoading}, 'button']" type="submit">Update settings</button>
+          <button :class="['ui', {'loading': isLoading}, 'button']" type="submit"><i18next path="Update settings"/></button>
         </form>
       </div>
       <div class="ui hidden divider"></div>
       <div class="ui small text container">
-        <h2 class="ui header">Change my password</h2>
+        <h2 class="ui header"><i18next path="Change my password"/></h2>
         <form class="ui form" @submit.prevent="submitPassword()">
           <div v-if="passwordError" class="ui negative message">
-            <div class="header">Cannot change your password</div>
+            <div class="header"><i18next path="Cannot change your password"/></div>
             <ul class="list">
-              <li v-if="passwordError == 'invalid_credentials'">Please double-check your password is correct</li>
+              <i18next tag="li" v-if="passwordError == 'invalid_credentials'" path="Please double-check your password is correct"/>
             </ul>
           </div>
           <div class="field">
-            <label>Old password</label>
+            <label><i18next path="Old password"/></label>
             <input
             required
             type="password"
@@ -43,7 +43,7 @@
             v-model="old_password">
           </div>
           <div class="field">
-            <label>New password</label>
+            <label><i18next path="New password"/></label>
             <input
             required
             type="password"
@@ -51,7 +51,7 @@
             placeholder="Enter your new password"
             v-model="new_password">
           </div>
-          <button :class="['ui', {'loading': isLoading}, 'button']" type="submit">Change password</button>
+          <button :class="['ui', {'loading': isLoading}, 'button']" type="submit"><i18next path="Change password"/></button>
         </form>
       </div>
     </div>
diff --git a/front/src/components/auth/Signup.vue b/front/src/components/auth/Signup.vue
index 39ca366631367afe452e3dddcaf54f56ad5f511a..57966264f99f0aa732f5ecd306bc6d875f9ca9fc 100644
--- a/front/src/components/auth/Signup.vue
+++ b/front/src/components/auth/Signup.vue
@@ -2,19 +2,19 @@
   <div class="main pusher" v-title="'Sign Up'">
     <div class="ui vertical stripe segment">
       <div class="ui small text container">
-        <h2>Create a funkwhale account</h2>
+        <h2><i18next path="Create a funkwhale account"/></h2>
         <form
           v-if="$store.state.instance.settings.users.registration_enabled.value"
           :class="['ui', {'loading': isLoadingInstanceSetting}, 'form']"
           @submit.prevent="submit()">
           <div v-if="errors.length > 0" class="ui negative message">
-            <div class="header">We cannot create your account</div>
+            <div class="header"><i18next path="We cannot create your account"/></div>
             <ul class="list">
               <li v-for="error in errors">{{ error }}</li>
             </ul>
           </div>
           <div class="field">
-            <label>Username</label>
+            <i18next tag="label" path="Username"/>
             <input
             ref="username"
             required
@@ -24,7 +24,7 @@
             v-model="username">
           </div>
           <div class="field">
-            <label>Email</label>
+            <i18next tag="label" path="Email"/>
             <input
             ref="email"
             required
@@ -33,7 +33,7 @@
             v-model="email">
           </div>
           <div class="field">
-            <label>Password</label>
+            <i18next tag="label" path="Password"/>
             <div class="ui action input">
               <input
               required
@@ -45,9 +45,9 @@
               </span>
             </div>
           </div>
-          <button :class="['ui', 'green', {'loading': isLoading}, 'button']" type="submit">Create my account</button>
+          <button :class="['ui', 'green', {'loading': isLoading}, 'button']" type="submit"><i18next path="Create my account"/></button>
         </form>
-        <p v-else>Registration is currently disabled on this instance, please try again later.</p>
+        <i18next v-else tag="p" path="Registration is currently disabled on this instance, please try again later."/>
       </div>
     </div>
   </div>
diff --git a/front/src/components/common/DangerousButton.vue b/front/src/components/common/DangerousButton.vue
index 525b4c48ff259babf599b54a2368ad2017c677d3..690291d5b1d08abaa60f8576fce71b25a73f1047 100644
--- a/front/src/components/common/DangerousButton.vue
+++ b/front/src/components/common/DangerousButton.vue
@@ -4,7 +4,7 @@
 
     <modal class="small" :show.sync="showModal">
       <div class="header">
-        <slot name="modal-header">Do you want to confirm this action?</slot>
+        <slot name="modal-header"><i18next path="Do you want to confirm this action?"/></slot>
       </div>
       <div class="scrolling content">
         <div class="description">
@@ -12,9 +12,9 @@
         </div>
       </div>
       <div class="actions">
-        <div class="ui cancel button">Cancel</div>
+        <div class="ui cancel button"><i18next path="Cancel"/></div>
         <div :class="['ui', 'confirm', color, 'button']" @click="confirm">
-          <slot name="modal-confirm">Confirm</slot>
+          <slot name="modal-confirm"><i18next path="Confirm"/></slot>
         </div>
       </div>
     </modal>
@@ -26,7 +26,7 @@ import Modal from '@/components/semantic/Modal'
 
 export default {
   props: {
-    action: {type: Function, required: true},
+    action: {type: Function, required: false},
     disabled: {type: Boolean, default: false},
     color: {type: String, default: 'red'}
   },
@@ -41,7 +41,10 @@ export default {
   methods: {
     confirm () {
       this.showModal = false
-      this.action()
+      this.$emit('confirm')
+      if (this.action) {
+        this.action()
+      }
     }
   }
 }
diff --git a/front/src/components/discussion/Comment.vue b/front/src/components/discussion/Comment.vue
index a3c5176ecb9b908817b72f62f884c6f87d2b9660..0c0678d86221ea023fa1a319ce541e63b293304f 100644
--- a/front/src/components/discussion/Comment.vue
+++ b/front/src/components/discussion/Comment.vue
@@ -11,11 +11,13 @@
         <span
           @click="collapsed = false"
           v-if="truncated && collapsed"
-          class="expand">Expand</span>
-        <span
+          class="expand"
+          path="Expand"/>
+        <i18next
           @click="collapsed = true"
           v-if="truncated && !collapsed"
-          class="collapse">Collapse</span>
+          class="collapse"
+          path="Collapse"/>
       </div>
     </div>
   </div>
diff --git a/front/src/components/favorites/List.vue b/front/src/components/favorites/List.vue
index dbef186b6b5c7d8a6aec611b798ab22a41f03362..d189f2b85ed3a5df25a8726c725d3452cf64e327 100644
--- a/front/src/components/favorites/List.vue
+++ b/front/src/components/favorites/List.vue
@@ -2,11 +2,13 @@
   <div class="main pusher" v-title="'Your Favorites'">
     <div class="ui vertical center aligned stripe segment">
       <div :class="['ui', {'active': isLoading}, 'inverted', 'dimmer']">
-        <div class="ui text loader">Loading your favorites...</div>
+        <div class="ui text loader"><i18next path="Loading your favorites..."/></div>
       </div>
       <h2 v-if="results" class="ui center aligned icon header">
         <i class="circular inverted heart pink icon"></i>
-        {{ $store.state.favorites.count }} favorites
+        <i18next path="{%0%} favorites">
+          {{ $store.state.favorites.count }}
+        </i18next>
       </h2>
       <radio-button type="favorites"></radio-button>
     </div>
@@ -14,7 +16,7 @@
       <div :class="['ui', {'loading': isLoading}, 'form']">
         <div class="fields">
           <div class="field">
-            <label>Ordering</label>
+            <i18next tag="label" path="Ordering"/>
             <select class="ui dropdown" v-model="ordering">
               <option v-for="option in orderingOptions" :value="option[0]">
                 {{ option[1] }}
@@ -22,14 +24,14 @@
             </select>
           </div>
           <div class="field">
-            <label>Ordering direction</label>
+            <i18next tag="label" path="Ordering direction"/>
             <select class="ui dropdown" v-model="orderingDirection">
-              <option value="">Ascending</option>
-              <option value="-">Descending</option>
+              <option value=""><i18next path="Ascending"/></option>
+              <option value="-"><i18next path="Descending"/></option>
             </select>
           </div>
           <div class="field">
-            <label>Results per page</label>
+            <i18next tag="label" path="Results per page"/>
             <select class="ui dropdown" v-model="paginateBy">
               <option :value="parseInt(12)">12</option>
               <option :value="parseInt(25)">25</option>
diff --git a/front/src/components/favorites/TrackFavoriteIcon.vue b/front/src/components/favorites/TrackFavoriteIcon.vue
index d4838ba5f336ff0375145ed5925029a19bf68587..d8d62afaaa363e520c45e600d18a18fded34cf1d 100644
--- a/front/src/components/favorites/TrackFavoriteIcon.vue
+++ b/front/src/components/favorites/TrackFavoriteIcon.vue
@@ -1,12 +1,8 @@
  <template>
   <button @click="$store.dispatch('favorites/toggle', track.id)" v-if="button" :class="['ui', 'pink', {'inverted': isFavorite}, {'favorited': isFavorite}, 'button']">
     <i class="heart icon"></i>
-    <template v-if="isFavorite">
-      In favorites
-    </template>
-    <template v-else>
-      Add to favorites
-    </template>
+    <i18next v-if="isFavorite" path="In favorites"/>
+    <i18next v-else path="Add to favorites"/>
   </button>
   <i v-else @click="$store.dispatch('favorites/toggle', track.id)" :class="['favorite-icon', 'heart', {'pink': isFavorite}, {'favorited': isFavorite}, 'link', 'icon']" :title="title"></i>
 </template>
@@ -20,9 +16,9 @@ export default {
   computed: {
     title () {
       if (this.isFavorite) {
-        return 'Remove from favorites'
+        return this.$t('Remove from favorites')
       } else {
-        return 'Add to favorites'
+        return this.$t('Add to favorites')
       }
     },
     isFavorite () {
diff --git a/front/src/components/federation/LibraryCard.vue b/front/src/components/federation/LibraryCard.vue
new file mode 100644
index 0000000000000000000000000000000000000000..757561fb352be6300ae376780239f6e5fbbda612
--- /dev/null
+++ b/front/src/components/federation/LibraryCard.vue
@@ -0,0 +1,119 @@
+<template>
+  <div class="ui card">
+    <div class="content">
+      <div class="header">
+        {{ displayName }}
+      </div>
+    </div>
+    <div class="content">
+      <span class="right floated" v-if="following">
+        <i class="check icon"></i><i18next path="Following"/>
+      </span>
+      <span class="right floated" v-else-if="manuallyApprovesFollowers">
+        <i class="lock icon"></i><i18next path="Followers only"/>
+      </span>
+      <span class="right floated" v-else>
+        <i class="open lock icon"></i><i18next path="Open"/>
+      </span>
+      <span v-if="totalItems">
+        <i class="music icon"></i>
+        <i18next path="{%0%} tracks">
+          {{ totalItems }}
+        </i18next>
+      </span>
+    </div>
+    <div class="extra content">
+      <template v-if="awaitingApproval">
+        <i class="clock icon"></i>
+        <i18next path="Follow request pending approval"/>
+      </template>
+      <div
+        v-if="!library"
+        @click="follow"
+        :disabled="isLoading"
+        :class="['ui', 'basic', {loading: isLoading}, 'green', 'button']">
+        <i18next v-if="manuallyApprovesFollowers" path="Send a follow request"/>
+        <i18next v-else path="Follow"/>
+      </div>
+      <router-link
+        v-else
+        class="ui basic button"
+        :to="{name: 'federation.libraries.detail', params: {id: library.uuid }}">
+        <i18next path="Detail"/>
+      </router-link>
+    </div>
+  </div>
+</template>
+
+<script>
+import axios from 'axios'
+
+export default {
+  props: ['libraryData', 'libraryInstance'],
+  data () {
+    return {
+      library: this.libraryInstance,
+      isLoading: false,
+      data: null,
+      errors: []
+    }
+  },
+  methods: {
+    follow () {
+      let params = {
+        'actor': this.libraryData['actor']['id'],
+        'autoimport': false,
+        'download_files': false,
+        'federation_enabled': true
+      }
+      let self = this
+      self.isLoading = true
+      axios.post('/federation/libraries/', params).then((response) => {
+        self.$emit('follow', {data: self.libraryData, library: response.data})
+        self.library = response.data
+        self.isLoading = false
+      }, error => {
+        self.isLoading = false
+        self.errors = error.backendErrors
+      })
+    }
+  },
+  computed: {
+    displayName () {
+      if (this.libraryData) {
+        return this.libraryData.display_name
+      } else {
+        return `${this.library.actor.preferred_username}@${this.library.actor.domain}`
+      }
+    },
+    manuallyApprovesFollowers () {
+      if (this.libraryData) {
+        return this.libraryData.actor.manuallyApprovesFollowers
+      } else {
+        return this.library.actor.manually_approves_followers
+      }
+    },
+    totalItems () {
+      if (this.libraryData) {
+        return this.libraryData.library.totalItems
+      } else {
+        return this.library.tracks_count
+      }
+    },
+    awaitingApproval () {
+      if (this.libraryData) {
+        return this.libraryData.local.awaiting_approval
+      } else {
+        return this.library.follow.approved === null
+      }
+    },
+    following () {
+      if (this.libraryData) {
+        return this.libraryData.local.following
+      } else {
+        return this.library.follow.approved
+      }
+    }
+  }
+}
+</script>
diff --git a/front/src/components/federation/LibraryFollowTable.vue b/front/src/components/federation/LibraryFollowTable.vue
new file mode 100644
index 0000000000000000000000000000000000000000..fd16d83710cc956def2a8ce46a1b0e7d4b3506c9
--- /dev/null
+++ b/front/src/components/federation/LibraryFollowTable.vue
@@ -0,0 +1,175 @@
+<template>
+  <div>
+    <div class="ui form">
+      <div class="fields">
+        <div class="ui six wide field">
+          <input type="text" v-model="search" placeholder="Search by username, domain..." />
+        </div>
+        <div class="ui four wide inline field">
+          <div class="ui checkbox">
+            <input v-model="pending" type="checkbox">
+            <label><i18next path="Pending approval"/></label>
+          </div>
+        </div>
+      </div>
+    </div>
+    <div class="ui hidden divider"></div>
+    <table v-if="result" class="ui very basic single line unstackable table">
+      <thead>
+        <tr>
+          <i18next tag="th" path="Actor"/>
+          <i18next tag="th" path="Creation date"/>
+          <i18next tag="th" path="Status"/>
+          <i18next tag="th" path="Actions"/>
+        </tr>
+      </thead>
+      <tbody>
+        <tr v-for="follow in result.results">
+          <td>
+            {{ follow.actor.preferred_username }}@{{ follow.actor.domain }}
+          </td>
+          <td>
+            <human-date :date="follow.creation_date"></human-date>
+          </td>
+          <td>
+            <template v-if="follow.approved === true">
+              <i class="check icon"></i><i18next path="Approved"/>
+            </template>
+            <template v-else-if="follow.approved === false">
+              <i class="x icon"></i><i18next path="Refused"/>
+            </template>
+            <template v-else>
+              <i class="clock icon"></i><i18next path="Pending"/>
+            </template>
+          </td>
+          <td>
+            <dangerous-button v-if="follow.approved !== false" class="tiny basic labeled icon" color='red' @confirm="updateFollow(follow, false)">
+              <i class="x icon"></i><i18next path="Deny"/>
+              <p slot="modal-header"><i18next path="Deny access?"/></p>
+              <p slot="modal-content">
+                <i18next path="By confirming, {%0%}@{%1%} will be denied access to your library.">
+                  {{ follow.actor.preferred_username }}
+                  {{ follow.actor.domain }}
+                </i18next>
+              </p>
+              <p slot="modal-confirm"><i18next path="Deny"/></p>
+            </dangerous-button>
+            <dangerous-button v-if="follow.approved !== true" class="tiny basic labeled icon" color='green' @confirm="updateFollow(follow, true)">
+              <i class="x icon"></i> <i18next path="Approve"/>
+              <p slot="modal-header"><i18next path="Approve access?"/></p>
+              <p slot="modal-content">
+                <i18next path="By confirming, {%0%}@{%1%} will be granted access to your library.">
+                  {{ follow.actor.preferred_username }}
+                  {{ follow.actor.domain }}
+                </i18next>
+              <p slot="modal-confirm"><i18next path="Approve"/></p>
+            </dangerous-button>
+          </td>
+        </tr>
+      </tbody>
+      <tfoot class="full-width">
+        <tr>
+          <th>
+            <pagination
+            v-if="result && result.results.length > 0"
+            @page-changed="selectPage"
+            :compact="true"
+            :current="page"
+            :paginate-by="paginateBy"
+            :total="result.count"
+            ></pagination>
+          </th>
+          <th v-if="result && result.results.length > 0">
+            <i18next path="Showing results {%0%}-{%1%} on {%2%}">
+              {{ ((page-1) * paginateBy) + 1 }}
+              {{ ((page-1) * paginateBy) + result.results.length }}
+              {{ result.count }}
+            </i18next>
+          </th>
+          <th></th>
+          <th></th>
+        </tr>
+      </tfoot>
+    </table>
+  </div>
+</template>
+
+<script>
+import axios from 'axios'
+import _ from 'lodash'
+
+import Pagination from '@/components/Pagination'
+
+export default {
+  props: {
+    filters: {type: Object, required: false, default: () => {}}
+  },
+  components: {
+    Pagination
+  },
+  data () {
+    return {
+      isLoading: false,
+      result: null,
+      page: 1,
+      paginateBy: 25,
+      search: '',
+      pending: false
+    }
+  },
+  created () {
+    this.fetchData()
+  },
+  methods: {
+    fetchData () {
+      let params = _.merge({
+        'page': this.page,
+        'page_size': this.paginateBy,
+        'q': this.search
+      }, this.filters)
+      if (this.pending) {
+        params.pending = true
+      }
+      let self = this
+      self.isLoading = true
+      axios.get('/federation/libraries/followers/', {params: params}).then((response) => {
+        self.result = response.data
+        self.isLoading = false
+      }, error => {
+        self.isLoading = false
+        self.errors = error.backendErrors
+      })
+    },
+    selectPage: function (page) {
+      this.page = page
+    },
+    updateFollow (follow, approved) {
+      let payload = {
+        follow: follow.id,
+        approved: approved
+      }
+      let self = this
+      axios.patch('/federation/libraries/followers/', payload).then((response) => {
+        follow.approved = response.data.approved
+        self.isLoading = false
+      }, error => {
+        self.isLoading = false
+        self.errors = error.backendErrors
+      })
+    }
+  },
+  watch: {
+    search (newValue) {
+      if (newValue.length > 0) {
+        this.fetchData()
+      }
+    },
+    page () {
+      this.fetchData()
+    },
+    pending () {
+      this.fetchData()
+    }
+  }
+}
+</script>
diff --git a/front/src/components/federation/LibraryForm.vue b/front/src/components/federation/LibraryForm.vue
new file mode 100644
index 0000000000000000000000000000000000000000..7e1d5c49f4a70c67fa64db3fefeae91b75dc28fb
--- /dev/null
+++ b/front/src/components/federation/LibraryForm.vue
@@ -0,0 +1,110 @@
+<template>
+  <form class="ui form" @submit.prevent="fetchInstanceInfo">
+    <h3 class="ui header"><i18next path="Federate with a new instance"/></h3>
+    <p><i18next path="Use this form to scan an instance and setup federation."/></p>
+    <div v-if="errors.length > 0 || scanErrors.length > 0" class="ui negative message">
+      <div class="header"><i18next path="Error while scanning library"/></div>
+      <ul class="list">
+        <li v-for="error in errors">{{ error }}</li>
+        <li v-for="error in scanErrors">{{ error }}</li>
+      </ul>
+    </div>
+    <div class="ui two fields">
+      <div class="ui field">
+        <label><i18next path="Library name"/></label>
+        <input v-model="libraryUsername" type="text" placeholder="library@demo.funkwhale.audio" />
+      </div>
+      <div class="ui field">
+        <label>&nbsp;</label>
+        <button
+          type="submit"
+          :disabled="isLoading"
+          :class="['ui', 'icon', {loading: isLoading}, 'button']">
+          <i class="search icon"></i>
+          <i18next path="Launch scan"/>
+        </button>
+      </div>
+    </div>
+  </form>
+</template>
+
+<script>
+import axios from 'axios'
+import TrackTable from '@/components/audio/track/Table'
+import RadioButton from '@/components/radios/Button'
+import Pagination from '@/components/Pagination'
+
+export default {
+  components: {
+    TrackTable,
+    RadioButton,
+    Pagination
+  },
+  data () {
+    return {
+      isLoading: false,
+      libraryUsername: '',
+      result: null,
+      errors: []
+    }
+  },
+  methods: {
+    follow () {
+      let params = {
+        'actor': this.result['actor']['id'],
+        'autoimport': false,
+        'download_files': false,
+        'federation_enabled': true
+      }
+      let self = this
+      self.isFollowing = false
+      axios.post('/federation/libraries/', params).then((response) => {
+        self.$emit('follow', {data: self.result, library: response.data})
+        self.result = response.data
+        self.isFollowing = false
+      }, error => {
+        self.isFollowing = false
+        self.errors = error.backendErrors
+      })
+    },
+    fetchInstanceInfo () {
+      let self = this
+      this.isLoading = true
+      self.errors = []
+      self.result = null
+      axios.get('/federation/libraries/fetch/', {params: {account: this.libraryUsername}}).then((response) => {
+        self.result = response.data
+        self.result.display_name = self.libraryUsername
+        self.isLoading = false
+      }, error => {
+        self.isLoading = false
+        self.errors = error.backendErrors
+      })
+    }
+  },
+  computed: {
+    scanErrors () {
+      let errors = []
+      if (!this.result) {
+        return errors
+      }
+      let keys = ['webfinger', 'actor', 'library']
+      keys.forEach(k => {
+        if (this.result[k]) {
+          if (this.result[k].errors) {
+            this.result[k].errors.forEach(e => {
+              errors.push(e)
+            })
+          }
+        }
+      })
+      return errors
+    }
+  },
+  watch: {
+    result (newValue, oldValue) {
+      this.$emit('scanned', newValue)
+    }
+  }
+}
+</script>
diff --git a/front/src/components/federation/LibraryTrackTable.vue b/front/src/components/federation/LibraryTrackTable.vue
new file mode 100644
index 0000000000000000000000000000000000000000..925ef3889668d5fbae39cf434fa8db390d0f4d06
--- /dev/null
+++ b/front/src/components/federation/LibraryTrackTable.vue
@@ -0,0 +1,207 @@
+<template>
+  <div>
+    <div class="ui inline form">
+      <div class="fields">
+        <div class="ui field">
+          <label>{{ $t('Search') }}</label>
+          <input type="text" v-model="search" placeholder="Search by title, artist, domain..." />
+        </div>
+        <div class="ui field">
+          <label>{{ $t('Import status') }}</label>
+          <select class="ui dropdown" v-model="importedFilter">
+            <option :value="null">{{ $t('Any') }}</option>
+            <option :value="true">{{ $t('Imported') }}</option>
+            <option :value="false">{{ $t('Not imported') }}</option>
+          </select>
+        </div>
+      </div>
+    </div>
+    <table v-if="result" class="ui compact very basic single line unstackable table">
+      <thead>
+        <tr>
+          <th>
+            <div class="ui checkbox">
+              <input
+                type="checkbox"
+                @change="toggleCheckAll"
+                :checked="result.results.length === checked.length"><label>&nbsp;</label>
+            </div>
+          </th>
+          <i18next tag="th" path="Title"/>
+          <i18next tag="th" path="Artist"/>
+          <i18next tag="th" path="Album"/>
+          <i18next tag="th" path="Published date"/>
+          <i18next tag="th" v-if="showLibrary" path="Library"/>
+        </tr>
+      </thead>
+      <tbody>
+        <tr v-for="track in result.results">
+          <td class="collapsing">
+            <div v-if="!track.local_track_file" class="ui checkbox">
+              <input
+                type="checkbox"
+                @change="toggleCheck(track.id)"
+                :checked="checked.indexOf(track.id) > -1"><label>&nbsp;</label>
+            </div>
+            <div v-else class="ui label">
+              <i18next path="In library"/>
+            </div>
+          </td>
+          <td>
+            <span :title="track.title">{{ track.title|truncate(30) }}</span>
+          </td>
+          <td>
+            <span :title="track.artist_name">{{ track.artist_name|truncate(30) }}</span>
+          </td>
+          <td>
+            <span :title="track.album_title">{{ track.album_title|truncate(20) }}</span>
+          </td>
+          <td>
+            <human-date :date="track.published_date"></human-date>
+          </td>
+          <td v-if="showLibrary">
+            {{ track.library.actor.domain }}
+          </td>
+        </tr>
+      </tbody>
+      <tfoot class="full-width">
+        <tr>
+          <th>
+            <pagination
+            v-if="result && result.results.length > 0"
+            @page-changed="selectPage"
+            :compact="true"
+            :current="page"
+            :paginate-by="paginateBy"
+            :total="result.count"
+            ></pagination>
+
+          </th>
+          <th v-if="result && result.results.length > 0">
+            {{ $t('Showing results {%start%}-{%end%} on {%total%}', {start: ((page-1) * paginateBy) + 1 , end: ((page-1) * paginateBy) + result.results.length, total: result.count})}}
+          <th>
+            <button
+              @click="launchImport"
+              :disabled="checked.length === 0 || isImporting"
+              :class="['ui', 'green', {loading: isImporting}, 'button']">
+              {{ $t('Import {%count%} tracks', {'count': checked.length}) }}
+            </button>
+            <router-link
+              v-if="importBatch"
+              :to="{name: 'library.import.batches.detail', params: {id: importBatch.id }}">
+              <i18next path="Import #{%id%} launched" :id="importBatch.id"/>
+            </router-link>
+          </th>
+          <th></th>
+          <th></th>
+          <th></th>
+          <th v-if="showLibrary"></th>
+        </tr>
+      </tfoot>
+    </table>
+  </div>
+</template>
+
+<script>
+import axios from 'axios'
+import _ from 'lodash'
+
+import Pagination from '@/components/Pagination'
+
+export default {
+  props: {
+    filters: {type: Object, required: false},
+    showLibrary: {type: Boolean, default: false}
+  },
+  components: {
+    Pagination
+  },
+  data () {
+    return {
+      isLoading: false,
+      result: null,
+      page: 1,
+      paginateBy: 25,
+      search: '',
+      checked: {},
+      isImporting: false,
+      importBatch: null,
+      importedFilter: null
+    }
+  },
+  created () {
+    this.fetchData()
+  },
+  methods: {
+    fetchData () {
+      let params = _.merge({
+        'page': this.page,
+        'page_size': this.paginateBy,
+        'q': this.search
+      }, this.filters)
+      if (this.importedFilter !== null) {
+        params.imported = this.importedFilter
+      }
+      let self = this
+      self.isLoading = true
+      self.checked = []
+      axios.get('/federation/library-tracks/', {params: params}).then((response) => {
+        self.result = response.data
+        self.isLoading = false
+      }, error => {
+        self.isLoading = false
+        self.errors = error.backendErrors
+      })
+    },
+    launchImport () {
+      let self = this
+      self.isImporting = true
+      let payload = {
+        library_tracks: this.checked
+      }
+      axios.post('/submit/federation/', payload).then((response) => {
+        self.importBatch = response.data
+        self.isImporting = false
+        self.fetchData()
+      }, error => {
+        self.isImporting = false
+        self.errors = error.backendErrors
+      })
+    },
+    toggleCheckAll () {
+      if (this.checked.length === this.result.results.length) {
+        // we uncheck
+        this.checked = []
+      } else {
+        this.checked = this.result.results.filter(t => {
+          return t.local_track_file === null
+        }).map(t => { return t.id })
+      }
+    },
+    toggleCheck (id) {
+      if (this.checked.indexOf(id) > -1) {
+        // we uncheck
+        this.checked.splice(this.checked.indexOf(id), 1)
+      } else {
+        this.checked.push(id)
+      }
+    },
+    selectPage: function (page) {
+      this.page = page
+    }
+  },
+  watch: {
+    search (newValue) {
+      if (newValue.length > 0) {
+        this.fetchData()
+      }
+    },
+    page () {
+      this.fetchData()
+    },
+    importedFilter () {
+      this.fetchData()
+    }
+  }
+}
+</script>
diff --git a/front/src/components/instance/Stats.vue b/front/src/components/instance/Stats.vue
index 884809f3a247515d5f2aebcc5d40ab6b14646d15..7da9fc6ede056c4174f5dc6004f5c6233664008b 100644
--- a/front/src/components/instance/Stats.vue
+++ b/front/src/components/instance/Stats.vue
@@ -2,32 +2,26 @@
   <div>
     <div v-if="stats" class="ui stackable two column grid">
       <div class="column">
-        <h3 class="ui left aligned header">User activity</h3>
+        <h3 class="ui left aligned header"><i18next path="User activity"/></h3>
         <div class="ui mini horizontal statistics">
           <div class="statistic">
             <div class="value">
               <i class="green user icon"></i>
               {{ stats.users }}
             </div>
-            <div class="label">
-              Users
-            </div>
+            <i18next tag="div" class="label" path="users"/>
           </div>
           <div class="statistic">
             <div class="value">
               <i class="orange sound icon"></i> {{ stats.listenings }}
             </div>
-            <div class="label">
-              tracks listened
-            </div>
+            <i18next tag="div" class="label" path="tracks listened"/>
           </div>
           <div class="statistic">
             <div class="value">
               <i class="pink heart icon"></i> {{ stats.track_favorites }}
             </div>
-            <div class="label">
-              Tracks favorited
-            </div>
+            <i18next tag="div" class="label" path="Tracks favorited"/>
           </div>
         </div>
       </div>
@@ -38,33 +32,25 @@
             <div class="value">
               {{ parseInt(stats.music_duration) }}
             </div>
-            <div class="label">
-              hours of music
-            </div>
+            <i18next tag="div" class="label" path="hours of music"/>
           </div>
           <div class="statistic">
             <div class="value">
               {{ stats.artists }}
             </div>
-            <div class="label">
-              Artists
-            </div>
+            <i18next tag="div" class="label" path="Artists"/>
           </div>
           <div class="statistic">
             <div class="value">
               {{ stats.albums }}
             </div>
-            <div class="label">
-              Albums
-            </div>
+            <i18next tag="div" class="label" path="Albums"/>
           </div>
           <div class="statistic">
             <div class="value">
               {{ stats.tracks }}
             </div>
-            <div class="label">
-              tracks
-            </div>
+            <i18next tag="div" class="label" path="tracks"/>
           </div>
         </div>
       </div>
diff --git a/front/src/components/library/Album.vue b/front/src/components/library/Album.vue
index 1e96757c342a9b325cb293f8363ea2078f358aff..1681d46e3b44466f022f18d22ffa3ba78e1643b7 100644
--- a/front/src/components/library/Album.vue
+++ b/front/src/components/library/Album.vue
@@ -10,30 +10,30 @@
             <i class="circular inverted sound yellow icon"></i>
             <div class="content">
               {{ album.title }}
-              <div class="sub header">
-                Album containing {{ album.tracks.length }} tracks,
-                by <router-link :to="{name: 'library.artists.detail', params: {id: album.artist.id }}">
+              <i18next tag="div" class="sub header" path="Album containing {%0%} tracks, by {%1%}">
+                {{ album.tracks.length }}
+                <router-link :to="{name: 'library.artists.detail', params: {id: album.artist.id }}">
                   {{ album.artist.name }}
                 </router-link>
-              </div>
+              </i18next>
             </div>
           </h2>
           <div class="ui hidden divider"></div>
           </button>
-          <play-button class="orange" :tracks="album.tracks">Play all</play-button>
+          <play-button class="orange" :tracks="album.tracks"><i18next path="Play all"/></play-button>
 
           <a :href="wikipediaUrl" target="_blank" class="ui button">
             <i class="wikipedia icon"></i>
-            Search on wikipedia
+            <i18next path="Search on Wikipedia"/>
           </a>
           <a :href="musicbrainzUrl" target="_blank" class="ui button">
             <i class="external icon"></i>
-            View on MusicBrainz
+            <i18next path="View on MusicBrainz"/>
           </a>
         </div>
       </div>
       <div class="ui vertical stripe segment">
-        <h2>Tracks</h2>
+        <h2><i18next path="Tracks"/></h2>
         <track-table v-if="album" :display-position="true" :tracks="album.tracks"></track-table>
       </div>
     </template>
diff --git a/front/src/components/library/Artist.vue b/front/src/components/library/Artist.vue
index 07d8cbbc74cff682e2251eb0c22515ca0fc8a9c0..5c17ac6af3d338fbdafff69511d7cc35c42877de 100644
--- a/front/src/components/library/Artist.vue
+++ b/front/src/components/library/Artist.vue
@@ -10,26 +10,31 @@
             <i class="circular inverted users violet icon"></i>
             <div class="content">
               {{ artist.name }}
-              <div class="sub header">{{ totalTracks }} tracks in {{ albums.length }} albums</div>
+              <div class="sub header">
+                <i18next path="{%0%} tracks in {%1%} albums">
+                  {{ totalTracks }}
+                  {{ albums.length }}
+                </i18next>
+              </div>
             </div>
           </h2>
           <div class="ui hidden divider"></div>
           <radio-button type="artist" :object-id="artist.id"></radio-button>
           </button>
-          <play-button class="orange" :tracks="allTracks">Play all albums</play-button>
+          <play-button class="orange" :tracks="allTracks"><i18next path="Play all albums"/></play-button>
 
           <a :href="wikipediaUrl" target="_blank" class="ui button">
             <i class="wikipedia icon"></i>
-            Search on wikipedia
+            <i18next path="Search on Wikipedia"/>
           </a>
           <a :href="musicbrainzUrl" target="_blank" class="ui button">
             <i class="external icon"></i>
-            View on MusicBrainz
+            <i18next path="View on MusicBrainz"/>
           </a>
         </div>
       </div>
       <div class="ui vertical stripe segment">
-        <h2>Albums by this artist</h2>
+        <h2><i18next path="Albums by this artist"/></h2>
         <div class="ui stackable doubling three column grid">
           <div class="column" :key="album.id" v-for="album in sortedAlbums">
             <album-card :mode="'rich'" class="fluid" :album="album"></album-card>
diff --git a/front/src/components/library/Artists.vue b/front/src/components/library/Artists.vue
index 72ad108f1d2d78ce32357753d163045284263715..0811f0aa79083da03b5c12b74795ef9ddefb7c8f 100644
--- a/front/src/components/library/Artists.vue
+++ b/front/src/components/library/Artists.vue
@@ -1,15 +1,15 @@
 <template>
   <div v-title="'Artists'">
     <div class="ui vertical stripe segment">
-      <h2 class="ui header">Browsing artists</h2>
+      <h2 class="ui header"><i18next path="Browsing artists"/></h2>
       <div :class="['ui', {'loading': isLoading}, 'form']">
         <div class="fields">
           <div class="field">
-            <label>Search</label>
+            <label><i18next path="Search"/></label>
             <input type="text" v-model="query" placeholder="Enter an artist name..."/>
           </div>
           <div class="field">
-            <label>Ordering</label>
+            <i18next tag="label" path="Ordering"/>
             <select class="ui dropdown" v-model="ordering">
               <option v-for="option in orderingOptions" :value="option[0]">
                 {{ option[1] }}
@@ -17,14 +17,14 @@
             </select>
           </div>
           <div class="field">
-            <label>Ordering direction</label>
+            <i18next tag="label" path="Ordering direction"/>
             <select class="ui dropdown" v-model="orderingDirection">
               <option value="">Ascending</option>
               <option value="-">Descending</option>
             </select>
           </div>
           <div class="field">
-            <label>Results per page</label>
+            <i18next tag="label" path="Results per page"/>
             <select class="ui dropdown" v-model="paginateBy">
               <option :value="parseInt(12)">12</option>
               <option :value="parseInt(25)">25</option>
diff --git a/front/src/components/library/Home.vue b/front/src/components/library/Home.vue
index 26352d1fd6c0314129dc7e212d7ddb7d97dd4519..e69ecea805c8fda726d3c02e50e46dc93d881c41 100644
--- a/front/src/components/library/Home.vue
+++ b/front/src/components/library/Home.vue
@@ -6,20 +6,20 @@
     <div class="ui vertical stripe segment">
       <div class="ui stackable three column grid">
         <div class="column">
-          <h2 class="ui header">Latest artists</h2>
+          <h2 class="ui header"><i18next path="Latest artists"/></h2>
           <div :class="['ui', {'active': isLoadingArtists}, 'inline', 'loader']"></div>
           <div v-if="artists.length > 0" v-for="artist in artists.slice(0, 3)" :key="artist.id" class="ui cards">
             <artist-card :artist="artist"></artist-card>
           </div>
         </div>
         <div class="column">
-          <h2 class="ui header">Radios</h2>
+          <h2 class="ui header"><i18next path="Radios"/></h2>
           <radio-card :type="'favorites'"></radio-card>
           <radio-card :type="'random'"></radio-card>
           <radio-card :type="'less-listened'"></radio-card>
         </div>
         <div class="column">
-          <h2 class="ui header">Music requests</h2>
+          <h2 class="ui header"><i18next path="Music requests"/></h2>
           <request-form v-if="$store.state.auth.authenticated"></request-form>
         </div>
       </div>
diff --git a/front/src/components/library/Library.vue b/front/src/components/library/Library.vue
index 161d4519b2fab561022fc4a8b8f706e67dc3258c..507ecd269cadb7897299ee7ab015bf9f90d0a2b4 100644
--- a/front/src/components/library/Library.vue
+++ b/front/src/components/library/Library.vue
@@ -1,17 +1,20 @@
 <template>
   <div class="main library pusher">
     <div class="ui secondary pointing menu">
-      <router-link class="ui item" to="/library" exact>Browse</router-link>
-      <router-link class="ui item" to="/library/artists" exact>Artists</router-link>
-      <router-link class="ui item" to="/library/radios" exact>Radios</router-link>
-      <router-link class="ui item" to="/library/playlists" exact>Playlists</router-link>
+      <router-link class="ui item" to="/library" exact><i18next path="Browse"/></router-link>
+      <router-link class="ui item" to="/library/artists" exact><i18next path="Artists"/></router-link>
+      <router-link class="ui item" to="/library/radios" exact><i18next path="Radios"/></router-link>
+      <router-link class="ui item" to="/library/playlists" exact><i18next path="Playlists"/></router-link>
       <div class="ui secondary right menu">
         <router-link v-if="$store.state.auth.authenticated" class="ui item" to="/library/requests/" exact>
-          Requests
+          <i18next path="Requests"/>
           <div class="ui teal label">{{ requestsCount }}</div>
         </router-link>
-        <router-link v-if="$store.state.auth.availablePermissions['import.launch']" class="ui item" to="/library/import/launch" exact>Import</router-link>
-        <router-link v-if="$store.state.auth.availablePermissions['import.launch']" class="ui item" to="/library/import/batches">Import batches
+        <router-link v-if="$store.state.auth.availablePermissions['import.launch']" class="ui item" to="/library/import/launch" exact>
+          <i18next path="Import"/>
+        </router-link>
+        <router-link v-if="$store.state.auth.availablePermissions['import.launch']" class="ui item" to="/library/import/batches">
+          <i18next path="Import batches"/>
         </router-link>
       </div>
     </div>
diff --git a/front/src/components/library/Radios.vue b/front/src/components/library/Radios.vue
index 7ab14ab14f8d3dbc4fa02a52c34758d69b38b5e9..9fcadf0a6101c518a7f7ecf8b738f6a21ae2e0b0 100644
--- a/front/src/components/library/Radios.vue
+++ b/front/src/components/library/Radios.vue
@@ -1,17 +1,19 @@
 <template>
   <div v-title="'Radios'">
     <div class="ui vertical stripe segment">
-      <h2 class="ui header">Browsing radios</h2>
-      <router-link class="ui green basic button" to="/library/radios/build" exact>Create your own radio</router-link>
+      <h2 class="ui header"><i18next path="Browsing radios"/></h2>
+      <router-link class="ui green basic button" to="/library/radios/build" exact>
+        <i18next path="Create your own radio"/>
+      </router-link>
       <div class="ui hidden divider"></div>
       <div :class="['ui', {'loading': isLoading}, 'form']">
         <div class="fields">
           <div class="field">
-            <label>Search</label>
+            <i18next tag="label" path="Search"/>
             <input type="text" v-model="query" placeholder="Enter a radio name..."/>
           </div>
           <div class="field">
-            <label>Ordering</label>
+            <i18next tag="label" path="Ordering"/>
             <select class="ui dropdown" v-model="ordering">
               <option v-for="option in orderingOptions" :value="option[0]">
                 {{ option[1] }}
@@ -19,14 +21,14 @@
             </select>
           </div>
           <div class="field">
-            <label>Ordering direction</label>
+            <i18next tag="label" path="Ordering direction"/>
             <select class="ui dropdown" v-model="orderingDirection">
-              <option value="">Ascending</option>
-              <option value="-">Descending</option>
+              <option value=""><i18next path="Ascending"/></option>
+              <option value="-"><i18next path="Descending"/></option>
             </select>
           </div>
           <div class="field">
-            <label>Results per page</label>
+            <i18next tag="label" path="Results per page"/>
             <select class="ui dropdown" v-model="paginateBy">
               <option :value="parseInt(12)">12</option>
               <option :value="parseInt(25)">25</option>
diff --git a/front/src/components/library/Track.vue b/front/src/components/library/Track.vue
index e614635c9877ca315743d16cf0d4ebba47679597..940086e02aa9263bcab6924a75c161aacb5aa5dd 100644
--- a/front/src/components/library/Track.vue
+++ b/front/src/components/library/Track.vue
@@ -11,18 +11,19 @@
             <div class="content">
               {{ track.title }}
               <div class="sub header">
-                From album
-                <router-link :to="{name: 'library.albums.detail', params: {id: track.album.id }}">
-                  {{ track.album.title }}
-                </router-link>
-                by <router-link :to="{name: 'library.artists.detail', params: {id: track.artist.id }}">
-                  {{ track.artist.name }}
-                </router-link>
+                <i18next path="From album {%0%} by {%1%}">
+                  <router-link :to="{name: 'library.albums.detail', params: {id: track.album.id }}">
+                    {{ track.album.title }}
+                  </router-link>
+                  <router-link :to="{name: 'library.artists.detail', params: {id: track.artist.id }}">
+                    {{ track.artist.name }}
+                  </router-link>
+                </i18next>
               </div>
             </div>
           </h2>
 
-          <play-button class="orange" :track="track">Play</play-button>
+          <play-button class="orange" :track="track"><i18next path="Play"/></play-button>
           <track-favorite-icon :track="track" :button="true"></track-favorite-icon>
           <track-playlist-icon
             :button="true"
@@ -31,32 +32,30 @@
 
           <a :href="wikipediaUrl" target="_blank" class="ui button">
             <i class="wikipedia icon"></i>
-            Search on wikipedia
+            <i18next path="Search on Wikipedia"/>
           </a>
           <a :href="musicbrainzUrl" target="_blank" class="ui button">
             <i class="external icon"></i>
-            View on MusicBrainz
+            <i18next path="View on MusicBrainz"/>
           </a>
           <a v-if="downloadUrl" :href="downloadUrl" target="_blank" class="ui button">
             <i class="download icon"></i>
-            Download
+            <i18next path="Download"/>
           </a>
         </div>
       </div>
       <div class="ui vertical stripe center aligned segment">
-        <h2>Lyrics</h2>
+        <h2><i18next path="Lyrics"/></h2>
         <div v-if="isLoadingLyrics" class="ui vertical segment">
           <div :class="['ui', 'centered', 'active', 'inline', 'loader']"></div>
         </div>
         <div v-if="lyrics" v-html="lyrics.content_rendered">
         </div>
         <template v-if="!isLoadingLyrics & !lyrics">
-          <p>
-            No lyrics available for this track.
-          </p>
+          <i18next tag="p" path="No lyrics available for this track."/>
           <a class="ui button" target="_blank" :href="lyricsSearchUrl">
             <i class="search icon"></i>
-            Search on lyrics.wikia.com
+            <i18next path="Search on lyrics.wikia.com"/>
           </a>
         </template>
       </div>
diff --git a/front/src/components/library/import/ArtistImport.vue b/front/src/components/library/import/ArtistImport.vue
index fb531439b4eb06c1afb1aab9be605ec5bc6219d6..b24be6dc831af6ad3b417aef6cce9a65907e0137 100644
--- a/front/src/components/library/import/ArtistImport.vue
+++ b/front/src/components/library/import/ArtistImport.vue
@@ -4,7 +4,7 @@
       <a :href="getMusicbrainzUrl('artist', metadata.id)" target="_blank" title="View on MusicBrainz">{{ metadata.name }}</a>
     </h3>
     <form class="ui form" @submit.prevent="">
-      <h6 class="ui header">Filter album types</h6>
+      <h6 class="ui header"><i18next path="Filter album types"/></h6>
       <div class="inline fields">
         <div class="field" v-for="t in availableReleaseTypes">
           <div class="ui checkbox">
@@ -13,7 +13,7 @@
           </div>
         </div>
         <div class="field">
-          <label>Query template</label>
+          <i18next tag="label" path="Query template"/>
           <input v-model="customQueryTemplate" />
         </div>
       </div>
@@ -54,7 +54,13 @@ export default Vue.extend({
       releaseGroupsData: {},
       releases: [],
       releaseTypes: ['Album'],
-      availableReleaseTypes: ['Album', 'Live', 'Compilation', 'EP', 'Single', 'Other']
+      availableReleaseTypes: [
+        'Album',
+        'Live',
+        'Compilation',
+        'EP',
+        'Single',
+        'Other']
     }
   },
   created () {
diff --git a/front/src/components/library/import/BatchDetail.vue b/front/src/components/library/import/BatchDetail.vue
index 362a5acd17972e6b26bc3deca8ae5881b2865e79..c7894fcc0249c4f27f58e6c6f29327b52f3929f5 100644
--- a/front/src/components/library/import/BatchDetail.vue
+++ b/front/src/components/library/import/BatchDetail.vue
@@ -20,11 +20,11 @@
       <table class="ui unstackable table">
         <thead>
           <tr>
-            <th>Job ID</th>
-            <th>Recording MusicBrainz ID</th>
-            <th>Source</th>
-            <th>Status</th>
-            <th>Track</th>
+            <i18next tag="th" path="Job ID"/>
+            <i18next tag="th" path="Recording MusicBrainz ID"/>
+            <i18next tag="th" path="Source"/>
+            <i18next tag="th" path="Status"/>
+            <i18next tag="th" path="Track"/>
           </tr>
         </thead>
         <tbody>
diff --git a/front/src/components/library/import/BatchList.vue b/front/src/components/library/import/BatchList.vue
index 857fef0457256aa7e7ea12f7d355bab1a36b32fe..324c3990a1494892435162bb52aa78fb516478c1 100644
--- a/front/src/components/library/import/BatchList.vue
+++ b/front/src/components/library/import/BatchList.vue
@@ -5,20 +5,20 @@
       <button
         class="ui left floated labeled icon button"
         @click="fetchData(previousLink)"
-        :disabled="!previousLink"><i class="left arrow icon"></i> Previous</button>
+        :disabled="!previousLink"><i class="left arrow icon"></i><i18next path="Previous"/></button>
       <button
         class="ui right floated right labeled icon button"
         @click="fetchData(nextLink)"
-        :disabled="!nextLink">Next <i class="right arrow icon"></i></button>
+        :disabled="!nextLink"><i18next path="Next"/><i class="right arrow icon"></i></button>
       <div class="ui hidden clearing divider"></div>
       <div class="ui hidden clearing divider"></div>
       <table v-if="results.length > 0" class="ui unstackable table">
         <thead>
           <tr>
-            <th>ID</th>
-            <th>Launch date</th>
-            <th>Jobs</th>
-            <th>Status</th>
+            <i18next tag="th" path="ID"/>
+            <i18next tag="th" path="Launch date"/>
+            <i18next tag="th" path="Jobs"/>
+            <i18next tag="th" path="Status"/>
           </tr>
         </thead>
         <tbody>
diff --git a/front/src/components/library/import/FileUpload.vue b/front/src/components/library/import/FileUpload.vue
index 35338c656170f3329a024d18f390916f74d81355..9a4b820e3fddac7a624999e49b64ed91ed84f777 100644
--- a/front/src/components/library/import/FileUpload.vue
+++ b/front/src/components/library/import/FileUpload.vue
@@ -17,29 +17,31 @@
         @input-file="inputFile"
         ref="upload">
         <i class="upload icon"></i>
-        Select files to upload...
+        <i18next path="Select files to upload..."/>
     </file-upload-widget>
       <button
         :class="['ui', 'right', 'floated', 'icon', {disabled: files.length === 0}, 'button']"
         v-if="!$refs.upload || !$refs.upload.active" @click.prevent="$refs.upload.active = true">
         <i class="play icon" aria-hidden="true"></i>
-        Start Upload
+        <i18next path="Start Upload"/>
       </button>
       <button type="button" class="ui right floated icon yellow button" v-else @click.prevent="$refs.upload.active = false">
         <i class="pause icon" aria-hidden="true"></i>
-        Stop Upload
+        <i18next path="Stop Upload"/>
       </button>
     </div>
     <div class="ui hidden clearing divider"></div>
-    <p v-if="batch">
-      Once all your files are uploaded, simply head over  <router-link :to="{name: 'library.import.batches.detail', params: {id: batch.id }}">import detail page</router-link> to check the import status.
-    </p>
+    <i18next v-if="batch" path="Once all your files are uploaded, simply head over {%0%} to check the import status.">
+      <router-link :to="{name: 'library.import.batches.detail', params: {id: batch.id }}">
+        <i18next path="import detail page"/>
+      </router-link>
+    </i18next>
     <table class="ui single line table">
       <thead>
         <tr>
-          <th>File name</th>
-          <th>Size</th>
-          <th>Status</th>
+          <i18next tag="th" path="File name"/>
+          <i18next tag="th" path="Size"/>
+          <i18next tag="th" path="Status"/>
         </tr>
       </thead>
       <tbody>
@@ -50,10 +52,10 @@
             <span v-if="file.error" class="ui red label">
               {{ file.error }}
             </span>
-            <span v-else-if="file.success" class="ui green label">Success</span>
-            <span v-else-if="file.active" class="ui yellow label">Uploading...</span>
+            <i18next v-else-if="file.success" class="ui green label" path="Success"/>
+            <i18next v-else-if="file.active" class="ui yellow label" path="Uploading..."/>
             <template v-else>
-              <span class="ui label">Pending</span>
+              <i18next class="ui label" path="Pending"/>
               <button class="ui tiny basic red icon button" @click.prevent="$refs.upload.remove(file)"><i class="delete icon"></i></button>
             </template>
           </td>
diff --git a/front/src/components/library/import/Main.vue b/front/src/components/library/import/Main.vue
index e2b6e484912d803a0494590afa04e6e8931401b8..de17e2afadaf655377896a029f01e4ab9945eefb 100644
--- a/front/src/components/library/import/Main.vue
+++ b/front/src/components/library/import/Main.vue
@@ -4,45 +4,47 @@
       <div class="ui top three attached ordered steps">
         <a @click="currentStep = 0" :class="['step', {'active': currentStep === 0}, {'completed': currentStep > 0}]">
           <div class="content">
-            <div class="title">Import source</div>
-            <div class="description">
-              Uploaded files or external source
-            </div>
+            <i18next tag="div" class="title" path="Import source"/>
+            <i18next tag="div" class="description" path="Uploaded files or external source"/>
           </div>
         </a>
         <a @click="currentStep = 1" :class="['step', {'active': currentStep === 1}, {'completed': currentStep > 1}]">
           <div class="content">
-            <div class="title">Metadata</div>
-            <div class="description">Grab corresponding metadata</div>
+            <i18next tag="div" class="title" path="Metadata"/>
+            <i18next tag="div" class="description" path="Grab corresponding metadata"/>
           </div>
         </a>
         <a @click="currentStep = 2" :class="['step', {'active': currentStep === 2}, {'completed': currentStep > 2}]">
           <div class="content">
-            <div class="title">Music</div>
-            <div class="description">Select relevant sources or files for import</div>
+            <i18next tag="div" class="title" path="Music"/>
+            <i18next tag="div" class="description" path="Select relevant sources or files for import"/>
           </div>
         </a>
       </div>
       <div class="ui hidden divider"></div>
       <div class="ui centered buttons">
-        <button @click="currentStep -= 1" :disabled="currentStep === 0" class="ui icon button"><i class="left arrow icon"></i> Previous step</button>
-        <button @click="currentStep += 1" v-if="currentStep < 2" class="ui icon button">Next step <i class="right arrow icon"></i></button>
+        <button @click="currentStep -= 1" :disabled="currentStep === 0" class="ui icon button"><i class="left arrow icon"></i><i18next path="Previous step"/></button>
+        <button @click="currentStep += 1" v-if="currentStep < 2" class="ui icon button"><i18next path="Next step"/><i class="right arrow icon"></i></button>
         <button
           @click="$refs.import.launchImport()"
           v-if="currentStep === 2"
           :class="['ui', 'positive', 'icon', {'loading': isImporting}, 'button']"
           :disabled="isImporting || importData.count === 0"
-          >Import {{ importData.count }} tracks <i class="check icon"></i></button>
+          >
+            <i18next path="Import {%0%} tracks">{{ importData.count }}</i18next>
+            <i class="check icon"></i>
+          </button>
       </div>
       <div class="ui hidden divider"></div>
       <div class="ui attached segment">
         <template v-if="currentStep === 0">
-          <p>First, choose where you want to import the music from :</p>
+          <i18next tag="p" path="First, choose where you want to import the music from"/>
           <form class="ui form">
             <div class="field">
               <div class="ui radio checkbox">
                 <input type="radio" id="external" value="external" v-model="currentSource">
-                <label for="external">External source. Supported backends:
+                <label for="external">
+                  <i18next path="External source. Supported backends"/>
                   <div v-for="backend in backends" class="ui basic label">
                     <i v-if="backend.icon" :class="[backend.icon, 'icon']"></i>
                     {{ backend.label }}
@@ -53,7 +55,7 @@
             <div class="field">
               <div class="ui radio checkbox">
                 <input type="radio" id="upload" value="upload" v-model="currentSource">
-                <label for="upload">File upload</label>
+                <i18next tag="label" for="upload" path="File upload" />
               </div>
             </div>
           </form>
@@ -62,7 +64,7 @@
           <div class="column">
             <form class="ui form" @submit.prevent="">
               <div class="field">
-                <label>Search an entity you want to import:</label>
+                <i18next tag="label" path="Search an entity you want to import:"/>
                 <metadata-search
                   :mb-type="mbType"
                   :mb-id="mbId"
@@ -70,29 +72,29 @@
                   @type-changed="updateType"></metadata-search>
               </div>
             </form>
-              <div class="ui horizontal divider">
-               Or
-             </div>
+            <i18next tag="div" class="ui horizontal divider" path="Or"/>
             <form class="ui form" @submit.prevent="">
               <div class="field">
-                <label>Input a MusicBrainz ID manually:</label>
+                <i18next tag="label" path="Input a MusicBrainz ID manually:"/>
                 <input type="text" v-model="currentId" />
               </div>
             </form>
             <div class="ui hidden divider"></div>
             <template v-if="currentType && currentId">
-              <h4 class="ui header">You will import:</h4>
+              <h4 class="ui header"><i18next path="You will import:"/></h4>
               <component
                 :mbId="currentId"
                 :is="metadataComponent"
                 @metadata-changed="this.updateMetadata"
                 ></component>
             </template>
-            <p>You can also skip this step and enter metadata manually.</p>
+            <i18next tag="p" path="You can also skip this step and enter metadata manually."/>
           </div>
           <div class="column">
             <h5 class="ui header">What is metadata?</h5>
-            <p>Metadata is the data related to the music you want to import. This includes all the information about the artists, albums and tracks. In order to have a high quality library, it is recommended to grab data from the <a href="http://musicbrainz.org/" target="_blank">MusicBrainz project</a>, which you can think about as the Wikipedia of music.</p>
+            <i18next tag="p" path="Metadata is the data related to the music you want to import. This includes all the information about the artists, albums and tracks. In order to have a high quality library, it is recommended to grab data from the {%0%} project, which you can think about as the Wikipedia of music.">
+              <a href="http://musicbrainz.org/" target="_blank">MusicBrainz</a>
+            </i18next>
           </div>
         </div>
         <div v-if="currentStep === 2">
@@ -116,8 +118,8 @@
       </div>
     </div>
     <div class="ui vertical stripe segment" v-if="currentRequest">
-      <h3 class="ui header">Music request</h3>
-      <p>This import will be associated with the music request below. After the import is finished, the request will be marked as fulfilled.</p>
+      <h3 class="ui header"><i18next path="Music request"/></h3>
+      <i18next tag="p" path="This import will be associated with the music request below. After the import is finished, the request will be marked as fulfilled."/>
       <request-card :request="currentRequest" :import-action="false"></request-card>
 
     </div>
diff --git a/front/src/components/library/import/ReleaseImport.vue b/front/src/components/library/import/ReleaseImport.vue
index 51d5a2fead24a91a50eb2f31f931e93f91fe01e3..4b792b3921a2fa091c4db82962e24e0d93258bb4 100644
--- a/front/src/components/library/import/ReleaseImport.vue
+++ b/front/src/components/library/import/ReleaseImport.vue
@@ -1,13 +1,16 @@
 <template>
   <div>
     <h3 class="ui dividing block header">
-      Album <a :href="getMusicbrainzUrl('release', metadata.id)" target="_blank" title="View on MusicBrainz">{{ metadata.title }}</a> ({{ tracks.length}} tracks) by
-      <a :href="getMusicbrainzUrl('artist', metadata['artist-credit'][0]['artist']['id'])" target="_blank" title="View on MusicBrainz">{{ metadata['artist-credit-phrase'] }}</a>
+      <i18next path="Album {%0%} ({%1%} tracks) by {%2%}">
+        <a :href="getMusicbrainzUrl('release', metadata.id)" target="_blank" title="View on MusicBrainz">{{ metadata.title }}</a>
+        ({{ tracks.length}} tracks)
+        <a :href="getMusicbrainzUrl('artist', metadata['artist-credit'][0]['artist']['id'])" target="_blank" title="View on MusicBrainz">{{ metadata['artist-credit-phrase'] }}</a>
+      </i18next>
       <div class="ui divider"></div>
       <div class="sub header">
         <div class="ui toggle checkbox">
           <input type="checkbox" v-model="enabled" />
-          <label>Import this release</label>
+          <i18next tag="label" path="Import this release"/>
         </div>
       </div>
     </h3>
diff --git a/front/src/components/library/import/TrackImport.vue b/front/src/components/library/import/TrackImport.vue
index f6adc5afbbbc77c99cca36ae19b04d1a2c449a7d..abb526eff0a8dbf69f81397d0136272637418429 100644
--- a/front/src/components/library/import/TrackImport.vue
+++ b/front/src/components/library/import/TrackImport.vue
@@ -9,13 +9,13 @@
       </h5>
       <div class="ui toggle checkbox">
         <input type="checkbox" v-model="enabled" />
-        <label>Import this track</label>
+        <i18next tag="label" path="Import this track"/>
       </div>
     </div>
     <div class="three wide column" v-if="enabled">
       <form class="ui mini form" @submit.prevent="">
         <div class="field">
-          <label>Source</label>
+          <i18next tag="label" path="Source"/>
           <select v-model="currentBackendId">
             <option v-for="backend in backends" :value="backend.id">
               {{ backend.label }}
@@ -28,7 +28,10 @@
         <button @click="currentResultIndex -= 1" class="ui basic tiny icon button" :disabled="currentResultIndex === 0">
           <i class="left arrow icon"></i>
         </button>
-        Result {{ currentResultIndex + 1 }}/{{ results.length }}
+        <i18next path="Result {%0%}/{%1%}">
+          {{ currentResultIndex + 1 }}
+          {{ results.length }}
+        </i18next>
         <button @click="currentResultIndex += 1" class="ui basic tiny icon button" :disabled="currentResultIndex + 1 === results.length">
           <i class="right arrow icon"></i>
         </button>
@@ -37,9 +40,9 @@
     <div class="four wide column" v-if="enabled">
       <form class="ui mini form" @submit.prevent="">
         <div class="field">
-          <label>Search query</label>
+          <i18next tag="label" path="Search query"/>
           <input type="text" v-model="query" />
-          <label>Imported URL</label>
+          <i18next tag="label" path="Imported URL"/>
           <input type="text" v-model="importedUrl" />
         </div>
       </form>
diff --git a/front/src/components/library/radios/Builder.vue b/front/src/components/library/radios/Builder.vue
index 5fbf0c992618616e0c0be0f76e458deb997737b8..85642b0a1235a61b859934df1fd46e5a2943dfc2 100644
--- a/front/src/components/library/radios/Builder.vue
+++ b/front/src/components/library/radios/Builder.vue
@@ -2,33 +2,30 @@
   <div class="ui vertical stripe segment" v-title="'Radio Builder'">
     <div>
       <div>
-        <h2 class="ui header">Builder</h2>
-        <p>
-          You can use this interface to build your own custom radio, which
-          will play tracks according to your criteria
-        </p>
-        <div class="ui form">
+        <h2 class="ui header"><i18next path="Builder"/></h2>
+        <i18next tag="p" path="You can use this interface to build your own custom radio, which will play tracks according to your criteria"/>
+          <div class="ui form">
           <div class="inline fields">
             <div class="field">
-              <label for="name">Radio name</label>
+              <i18next tag="label" for="name" path="Radio name"/>
               <input id="name" type="text" v-model="radioName" placeholder="My awesome radio" />
             </div>
             <div class="field">
               <input id="public" type="checkbox" v-model="isPublic" />
-              <label for="public">Display publicly</label>
+              <i18next tag="label" for="public" path="Display publicly"/>
             </div>
-            <button :disabled="!canSave" @click="save" class="ui green button">Save</button>
+            <button :disabled="!canSave" @click="save" class="ui green button"><i18ext path="Save"/></button>
             <radio-button v-if="id" type="custom" :custom-radio-id="id"></radio-button>
           </div>
         </div>
         <div class="ui form">
-          <p>Add filters to customize your radio</p>
+          <p><i18next path="Add filters to customize your radio"/></p>
           <div class="inline field">
             <select class="ui dropdown" v-model="currentFilterType">
-              <option value="">Select a filter</option>
+              <option value=""><i18next path="Select a filter"/></option>
               <option v-for="f in availableFilters" :value="f.type">{{ f.label }}</option>
             </select>
-            <button :disabled="!currentFilterType" @click="add" class="ui button">Add filter</button>
+            <button :disabled="!currentFilterType" @click="add" class="ui button"><i18next path="Add filter"/></button>
           </div>
           <p v-if="currentFilter">
             {{ currentFilter.help_text }}
@@ -37,11 +34,11 @@
         <table class="ui table">
           <thead>
             <tr>
-              <th class="two wide">Filter name</th>
-              <th class="one wide">Exclude</th>
-              <th class="six wide">Config</th>
-              <th class="five wide">Candidates</th>
-              <th class="two wide">Actions</th>
+              <i18next tag="th" class="two wide" path="Filter name"/>
+              <i18next tag="th" class="one wide" path="Exclude"/>
+              <i18next tag="th" class="six wide" path="Config"/>
+              <i18next tag="th" class="five wide" path="Candidates"/>
+              <i18next tag="th" class="two wide" path="Actions"/>
             </tr>
           </thead>
           <tbody>
@@ -57,9 +54,9 @@
           </tbody>
         </table>
         <template v-if="checkResult">
-          <h3 class="ui header">
-            {{ checkResult.candidates.count }} tracks matching combined filters
-          </h3>
+          <i18next tag="h3" class="ui header" path="{%0%} tracks matching combined filters">
+            {{ checkResult.candidates.count }}
+          </i18next>
           <track-table v-if="checkResult.candidates.sample" :tracks="checkResult.candidates.sample"></track-table>
         </template>
       </div>
diff --git a/front/src/components/library/radios/Filter.vue b/front/src/components/library/radios/Filter.vue
index 0b71115fd25e943be39d837ad4d3ec12f10866b2..c7f981ede934084606b07d068be5f88a9fdfa9c9 100644
--- a/front/src/components/library/radios/Filter.vue
+++ b/front/src/components/library/radios/Filter.vue
@@ -42,7 +42,7 @@
       </span>
       <modal v-if="checkResult" :show.sync="showCandidadesModal">
         <div class="header">
-          Track matching filter
+          <i18next path="Track matching filter"/>
         </div>
         <div class="content">
           <div class="description">
@@ -51,13 +51,13 @@
         </div>
         <div class="actions">
           <div class="ui black deny button">
-            Cancel
+            <i18next path="Cancel"/>
           </div>
         </div>
       </modal>
     </td>
     <td>
-      <button @click="$emit('delete', index)" class="ui basic red button">Remove</button>
+      <button @click="$emit('delete', index)" class="ui basic red button"><i18next path="Remove"/></button>
     </td>
   </tr>
 </template>
diff --git a/front/src/main.js b/front/src/main.js
index 5030e5c9c429d62eda3d6c20e86acc1d4e19a3c2..0c41294113929d4421a6ee36808be445de7ada5d 100644
--- a/front/src/main.js
+++ b/front/src/main.js
@@ -11,6 +11,9 @@ import router from './router'
 import axios from 'axios'
 import {VueMasonryPlugin} from 'vue-masonry'
 import VueLazyload from 'vue-lazyload'
+import i18next from 'i18next'
+import i18nextFetch from 'i18next-fetch-backend'
+import VueI18Next from '@panter/vue-i18next'
 import store from './store'
 import config from './config'
 import { sync } from 'vuex-router-sync'
@@ -27,6 +30,7 @@ window.$ = window.jQuery = require('jquery')
 require('semantic-ui-css/semantic.js')
 require('masonry-layout')
 
+Vue.use(VueI18Next)
 Vue.use(VueMasonryPlugin)
 Vue.use(VueLazyload)
 Vue.config.productionTip = false
@@ -71,17 +75,36 @@ axios.interceptors.response.use(function (response) {
     }
   }
   if (error.backendErrors.length === 0) {
-    error.backendErrors.push('An unknown error occured, ensure your are connected to the internet and your funkwhale instance is up and running')
+    error.backendErrors.push(i18next.t('An unknown error occured, ensure your are connected to the internet and your funkwhale instance is up and running'))
   }
   // Do something with response error
   return Promise.reject(error)
 })
 store.dispatch('auth/check')
+
+// i18n
+i18next
+  .use(i18nextFetch)
+  .init({
+    lng: navigator.language,
+    fallbackLng: ['en'],
+    preload: [navigator.language, 'en'],
+    backend: {
+      loadPath: '/static/translations/{%lng%}.json'
+    },
+    interpolation: {
+      prefix: '{%',
+      suffix: '%}'
+    }
+  })
+const i18n = new VueI18Next(i18next)
+
 /* eslint-disable no-new */
 new Vue({
   el: '#app',
   router,
   store,
+  i18n,
   template: '<App/>',
   components: { App }
 })
diff --git a/front/src/router/index.js b/front/src/router/index.js
index d41764227bb1a1cbd3536791ae24275b7228d249..a2bf781956a3350d514d108f5a8812be2e5f6156 100644
--- a/front/src/router/index.js
+++ b/front/src/router/index.js
@@ -25,6 +25,12 @@ import RequestsList from '@/components/requests/RequestsList'
 import PlaylistDetail from '@/views/playlists/Detail'
 import PlaylistList from '@/views/playlists/List'
 import Favorites from '@/components/favorites/List'
+import FederationBase from '@/views/federation/Base'
+import FederationScan from '@/views/federation/Scan'
+import FederationLibraryDetail from '@/views/federation/LibraryDetail'
+import FederationLibraryList from '@/views/federation/LibraryList'
+import FederationTrackList from '@/views/federation/LibraryTrackList'
+import FederationFollowersList from '@/views/federation/LibraryFollowersList'
 
 Vue.use(Router)
 
@@ -83,6 +89,50 @@ export default new Router({
         defaultPaginateBy: route.query.paginateBy
       })
     },
+    {
+      path: '/manage/federation',
+      component: FederationBase,
+      children: [
+        {
+          path: 'scan',
+          name: 'federation.libraries.scan',
+          component: FederationScan },
+        {
+          path: 'libraries',
+          name: 'federation.libraries.list',
+          component: FederationLibraryList,
+          props: (route) => ({
+            defaultOrdering: route.query.ordering,
+            defaultQuery: route.query.query,
+            defaultPaginateBy: route.query.paginateBy,
+            defaultPage: route.query.page
+          })
+        },
+        {
+          path: 'tracks',
+          name: 'federation.tracks.list',
+          component: FederationTrackList,
+          props: (route) => ({
+            defaultOrdering: route.query.ordering,
+            defaultQuery: route.query.query,
+            defaultPaginateBy: route.query.paginateBy,
+            defaultPage: route.query.page
+          })
+        },
+        {
+          path: 'followers',
+          name: 'federation.followers.list',
+          component: FederationFollowersList,
+          props: (route) => ({
+            defaultOrdering: route.query.ordering,
+            defaultQuery: route.query.query,
+            defaultPaginateBy: route.query.paginateBy,
+            defaultPage: route.query.page
+          })
+        },
+        { path: 'libraries/:id', name: 'federation.libraries.detail', component: FederationLibraryDetail, props: true }
+      ]
+    },
     {
       path: '/library',
       component: Library,
diff --git a/front/src/views/federation/Base.vue b/front/src/views/federation/Base.vue
new file mode 100644
index 0000000000000000000000000000000000000000..7958bb36b65332b840111cba5ab37c5e2a7962b0
--- /dev/null
+++ b/front/src/views/federation/Base.vue
@@ -0,0 +1,55 @@
+<template>
+  <div class="main pusher"  v-title="'Federation'">
+    <div class="ui secondary pointing menu">
+      <router-link
+        class="ui item"
+        :to="{name: 'federation.libraries.list'}">Libraries</router-link>
+      <router-link
+        class="ui item"
+        :to="{name: 'federation.tracks.list'}">Tracks</router-link>
+        <div class="ui secondary right menu">
+          <router-link
+            class="ui item"
+            :to="{name: 'federation.followers.list'}">
+            Followers
+            <div class="ui teal label" title="Pending requests">{{ requestsCount }}</div>
+          </router-link>
+        </div>
+    </div>
+    <router-view :key="$route.fullPath"></router-view>
+  </div>
+</template>
+<script>
+import axios from 'axios'
+export default {
+  data () {
+    return {
+      requestsCount: 0
+    }
+  },
+  created () {
+    this.fetchRequestsCount()
+  },
+  methods: {
+    fetchRequestsCount () {
+      let self = this
+      axios.get('federation/libraries/followers/', {params: {pending: true}}).then(response => {
+        self.requestsCount = response.data.count
+      })
+    }
+  }
+}
+</script>
+<style lang="scss">
+@import '../../style/vendor/media';
+
+.main.pusher > .ui.secondary.menu {
+  @include media(">tablet") {
+    margin: 0 2.5rem;
+  }
+  .item {
+    padding-top: 1.5em;
+    padding-bottom: 1.5em;
+  }
+}
+</style>
diff --git a/front/src/views/federation/LibraryDetail.vue b/front/src/views/federation/LibraryDetail.vue
new file mode 100644
index 0000000000000000000000000000000000000000..20250e333d866941e7605667b1a3541b7095e716
--- /dev/null
+++ b/front/src/views/federation/LibraryDetail.vue
@@ -0,0 +1,197 @@
+<template>
+  <div>
+    <div v-if="isLoading" class="ui vertical segment" v-title="'Library'">
+      <div :class="['ui', 'centered', 'active', 'inline', 'loader']"></div>
+    </div>
+    <template v-if="object">
+      <div :class="['ui', 'head', 'vertical', 'center', 'aligned', 'stripe', 'segment']" v-title="libraryUsername">
+        <div class="segment-content">
+          <h2 class="ui center aligned icon header">
+            <i class="circular inverted cloud olive icon"></i>
+            <div class="content">
+              {{ libraryUsername }}
+            </div>
+          </h2>
+        </div>
+        <div class="ui hidden divider"></div>
+        <div class="ui one column centered grid">
+          <table class="ui collapsing very basic table">
+            <tbody>
+              <tr>
+                <td >
+                  Follow status
+                  <span :data-tooltip="$t('This indicate if the remote library granted you access')"><i class="question circle icon"></i></span>
+                </td>
+                <td>
+                  <template v-if="object.follow.approved === null">
+                    <i class="loading icon"></i> Pending approval
+                  </template>
+                  <template v-else-if="object.follow.approved === true">
+                    <i class="check icon"></i> Following
+                  </template>
+                  <template v-else-if="object.follow.approved === false">
+                    <i class="x icon"></i> Not following
+                  </template>
+                </td>
+                <td>
+                </td>
+              </tr>
+              <tr>
+                <td>
+                  Federation
+                  <span :data-tooltip="$t('Use this flag to enable/disable federation with this library')"><i class="question circle icon"></i></span>
+                </td>
+                <td>
+                  <div class="ui toggle checkbox">
+                    <input
+                      @change="update('federation_enabled')"
+                      v-model="object.federation_enabled" type="checkbox">
+                    <label></label>
+                  </div>
+                </td>
+                <td>
+                </td>
+              </tr>
+              <tr>
+                <td>
+                  Auto importing
+                  <span :data-tooltip="$t('When enabled, auto importing will automatically import new tracks published in this library')"><i class="question circle icon"></i></span>
+                </td>
+                <td>
+                  <div class="ui toggle checkbox">
+                    <input
+                      @change="update('autoimport')"
+                      v-model="object.autoimport" type="checkbox">
+                    <label></label>
+                  </div>
+                </td>
+                <td></td>
+              </tr>
+              <!-- Disabled until properly implemented on the backend
+              <tr>
+                <td>File mirroring</td>
+                <td>
+                  <div class="ui toggle checkbox">
+                    <input
+                      @change="update('download_files')"
+                      v-model="object.download_files" type="checkbox">
+                    <label></label>
+                  </div>
+                </td>
+                <td></td>
+              </tr>
+              -->
+              <tr>
+                <td>Library size</td>
+                <td>
+                  {{ object.tracks_count }} tracks
+                </td>
+                <td></td>
+              </tr>
+              <tr>
+                <td>Last fetched</td>
+                <td>
+                  <human-date v-if="object.fetched_date" :date="object.fetched_date"></human-date>
+                  <template v-else>Never</template>
+                  <button
+                    @click="scan"
+                    v-if="!scanTrigerred"
+                    :class="['ui', 'basic', {loading: isScanLoading}, 'button']">
+                    <i class="sync icon"></i> Trigger scan
+                  </button>
+                  <button v-else class="ui success button">
+                    <i class="check icon"></i> Scan triggered!
+                  </button>
+
+                </td>
+                <td></td>
+              </tr>
+            </tbody>
+          </table>
+        </div>
+        <div class="ui hidden divider"></div>
+        <button @click="fetchData" class="ui basic button">Refresh</button>
+      </div>
+      <div class="ui vertical stripe segment">
+        <h2>Tracks available in this library</h2>
+        <library-track-table v-if="!isLoading" :filters="{library: id}"></library-track-table>
+      </div>
+    </template>
+  </div>
+</template>
+
+<script>
+import axios from 'axios'
+import logger from '@/logging'
+
+import LibraryTrackTable from '@/components/federation/LibraryTrackTable'
+
+export default {
+  props: ['id'],
+  components: {
+    LibraryTrackTable
+  },
+  data () {
+    return {
+      isLoading: true,
+      isScanLoading: false,
+      object: null,
+      scanTrigerred: false
+    }
+  },
+  created () {
+    this.fetchData()
+  },
+  methods: {
+    fetchData () {
+      var self = this
+      this.isLoading = true
+      let url = 'federation/libraries/' + this.id + '/'
+      logger.default.debug('Fetching library "' + this.id + '"')
+      axios.get(url).then((response) => {
+        self.object = response.data
+        self.isLoading = false
+      })
+    },
+    scan (until) {
+      var self = this
+      this.isScanLoading = true
+      let data = {}
+      let url = 'federation/libraries/' + this.id + '/scan/'
+      logger.default.debug('Triggering scan for library "' + this.id + '"')
+      axios.post(url, data).then((response) => {
+        self.scanTrigerred = true
+        logger.default.debug('Scan triggered with id', response.data)
+        self.isScanLoading = false
+      })
+    },
+    update (attr) {
+      let newValue = this.object[attr]
+      let params = {}
+      let self = this
+      params[attr] = newValue
+      axios.patch('federation/libraries/' + this.id + '/', params).then((response) => {
+        logger.default.info(`${attr} was updated succcessfully to ${newValue}`)
+      }, (error) => {
+        logger.default.error(`Error while setting ${attr} to ${newValue}`, error)
+        self.object[attr] = !newValue
+      })
+    }
+  },
+  computed: {
+    libraryUsername () {
+      let actor = this.object.actor
+      return `${actor.preferred_username}@${actor.domain}`
+    }
+  },
+  watch: {
+    id () {
+      this.fetchData()
+    }
+  }
+}
+</script>
+
+<!-- Add "scoped" attribute to limit CSS to this component only -->
+<style scoped>
+</style>
diff --git a/front/src/views/federation/LibraryFollowersList.vue b/front/src/views/federation/LibraryFollowersList.vue
new file mode 100644
index 0000000000000000000000000000000000000000..8ca120e8b54e3178d16dfcd7cd6f4af9468ddf1d
--- /dev/null
+++ b/front/src/views/federation/LibraryFollowersList.vue
@@ -0,0 +1,27 @@
+<template>
+  <div v-title="'Followers'">
+    <div class="ui vertical stripe segment">
+      <h2 class="ui header">Browsing followers</h2>
+      <p>
+        Be careful when accepting follow requests, as it means the follower
+        will have access to your entire library.
+      </p>
+      <div class="ui hidden divider"></div>
+      <library-follow-table></library-follow-table>
+    </div>
+  </div>
+</template>
+
+<script>
+import LibraryFollowTable from '@/components/federation/LibraryFollowTable'
+
+export default {
+  components: {
+    LibraryFollowTable
+  }
+}
+</script>
+
+<!-- Add "scoped" attribute to limit CSS to this component only -->
+<style scoped>
+</style>
diff --git a/front/src/views/federation/LibraryList.vue b/front/src/views/federation/LibraryList.vue
new file mode 100644
index 0000000000000000000000000000000000000000..43800a72e4afd182b3b925997f842f11198bf416
--- /dev/null
+++ b/front/src/views/federation/LibraryList.vue
@@ -0,0 +1,172 @@
+<template>
+  <div v-title="'Libraries'">
+    <div class="ui vertical stripe segment">
+      <h2 class="ui header">Browsing libraries</h2>
+      <router-link
+        class="ui basic green button"
+        :to="{name: 'federation.libraries.scan'}">
+        <i class="plus icon"></i>
+        Add a new library
+      </router-link>
+      <div class="ui hidden divider"></div>
+      <div :class="['ui', {'loading': isLoading}, 'form']">
+        <div class="fields">
+          <div class="field">
+            <label>Search</label>
+            <input type="text" v-model="query" placeholder="Enter an library domain name..."/>
+          </div>
+          <div class="field">
+            <label>Ordering</label>
+            <select class="ui dropdown" v-model="ordering">
+              <option v-for="option in orderingOptions" :value="option[0]">
+                {{ option[1] }}
+              </option>
+            </select>
+          </div>
+          <div class="field">
+            <label>Ordering direction</label>
+            <select class="ui dropdown" v-model="orderingDirection">
+              <option value="">Ascending</option>
+              <option value="-">Descending</option>
+            </select>
+          </div>
+          <div class="field">
+            <label>Results per page</label>
+            <select class="ui dropdown" v-model="paginateBy">
+              <option :value="parseInt(12)">12</option>
+              <option :value="parseInt(25)">25</option>
+              <option :value="parseInt(50)">50</option>
+            </select>
+          </div>
+        </div>
+      </div>
+      <div class="ui hidden divider"></div>
+      <div
+        v-if="result"
+        v-masonry
+        transition-duration="0"
+        item-selector=".column"
+        percent-position="true"
+        stagger="0"
+        class="ui stackable three column doubling grid">
+        <div
+          v-masonry-tile
+          v-if="result.results.length > 0"
+          v-for="library in result.results"
+          :key="library.id"
+          class="column">
+          <library-card class="fluid" :library-instance="library"></library-card>
+        </div>
+      </div>
+      <div class="ui center aligned basic segment">
+        <pagination
+          v-if="result && result.results.length > 0"
+          @page-changed="selectPage"
+          :current="page"
+          :paginate-by="paginateBy"
+          :total="result.count"
+          ></pagination>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+import axios from 'axios'
+import _ from 'lodash'
+import $ from 'jquery'
+
+import logger from '@/logging'
+
+import OrderingMixin from '@/components/mixins/Ordering'
+import PaginationMixin from '@/components/mixins/Pagination'
+import LibraryCard from '@/components/federation/LibraryCard'
+import Pagination from '@/components/Pagination'
+
+const FETCH_URL = 'federation/libraries/'
+
+export default {
+  mixins: [OrderingMixin, PaginationMixin],
+  props: {
+    defaultQuery: {type: String, required: false, default: ''}
+  },
+  components: {
+    LibraryCard,
+    Pagination
+  },
+  data () {
+    let defaultOrdering = this.getOrderingFromString(this.defaultOrdering || '-creation_date')
+    return {
+      isLoading: true,
+      result: null,
+      page: parseInt(this.defaultPage),
+      query: this.defaultQuery,
+      paginateBy: parseInt(this.defaultPaginateBy || 50),
+      orderingDirection: defaultOrdering.direction,
+      ordering: defaultOrdering.field,
+      orderingOptions: [
+        ['creation_date', 'Creation date'],
+        ['tracks_count', 'Available tracks']
+      ]
+    }
+  },
+  created () {
+    this.fetchData()
+  },
+  mounted () {
+    $('.ui.dropdown').dropdown()
+  },
+  methods: {
+    updateQueryString: _.debounce(function () {
+      this.$router.replace({
+        query: {
+          query: this.query,
+          page: this.page,
+          paginateBy: this.paginateBy,
+          ordering: this.getOrderingAsString()
+        }
+      })
+    }, 500),
+    fetchData: _.debounce(function () {
+      var self = this
+      this.isLoading = true
+      let url = FETCH_URL
+      let params = {
+        page: this.page,
+        q: this.query,
+        ordering: this.getOrderingAsString()
+      }
+      logger.default.debug('Fetching libraries')
+      axios.get(url, {params: params}).then((response) => {
+        self.result = response.data
+        self.isLoading = false
+      })
+    }, 500),
+    selectPage: function (page) {
+      this.page = page
+    }
+  },
+  watch: {
+    page () {
+      this.updateQueryString()
+      this.fetchData()
+    },
+    ordering () {
+      this.updateQueryString()
+      this.fetchData()
+    },
+    orderingDirection () {
+      this.updateQueryString()
+      this.fetchData()
+    },
+    query () {
+      this.updateQueryString()
+      this.fetchData()
+    }
+  }
+}
+</script>
+
+<!-- Add "scoped" attribute to limit CSS to this component only -->
+<style scoped>
+</style>
diff --git a/front/src/views/federation/LibraryTrackList.vue b/front/src/views/federation/LibraryTrackList.vue
new file mode 100644
index 0000000000000000000000000000000000000000..42526b6e422d2325c480024e035d9907a28e9fe0
--- /dev/null
+++ b/front/src/views/federation/LibraryTrackList.vue
@@ -0,0 +1,23 @@
+<template>
+  <div v-title="'Federated tracks'">
+    <div class="ui vertical stripe segment">
+      <h2 class="ui header">Browsing federated tracks</h2>
+      <div class="ui hidden divider"></div>
+      <library-track-table :show-library="true"></library-track-table>
+    </div>
+  </div>
+</template>
+
+<script>
+import LibraryTrackTable from '@/components/federation/LibraryTrackTable'
+
+export default {
+  components: {
+    LibraryTrackTable
+  }
+}
+</script>
+
+<!-- Add "scoped" attribute to limit CSS to this component only -->
+<style scoped>
+</style>
diff --git a/front/src/views/federation/Scan.vue b/front/src/views/federation/Scan.vue
new file mode 100644
index 0000000000000000000000000000000000000000..5caa2f5405b43a4194e9571edd22c2277a9adc36
--- /dev/null
+++ b/front/src/views/federation/Scan.vue
@@ -0,0 +1,39 @@
+<template>
+  <div>
+    <div class="ui vertical stripe segment">
+      <library-form @scanned="updateLibraryData"></library-form>
+      <library-card v-if="libraryData" :library-data="libraryData"></library-card>
+    </div>
+    <div class="ui vertical stripe segment">
+    </div>
+  </div>
+</template>
+
+<script>
+// import axios from 'axios'
+import TrackTable from '@/components/audio/track/Table'
+import RadioButton from '@/components/radios/Button'
+import Pagination from '@/components/Pagination'
+import LibraryForm from '@/components/federation/LibraryForm'
+import LibraryCard from '@/components/federation/LibraryCard'
+
+export default {
+  components: {
+    TrackTable,
+    RadioButton,
+    Pagination,
+    LibraryForm,
+    LibraryCard
+  },
+  data () {
+    return {
+      libraryData: null
+    }
+  },
+  methods: {
+    updateLibraryData (data) {
+      this.libraryData = data
+    }
+  }
+}
+</script>
diff --git a/front/vendor/vue-i18n-xgettext/extractor.js b/front/vendor/vue-i18n-xgettext/extractor.js
new file mode 100644
index 0000000000000000000000000000000000000000..e8f2bde4faaa838cc692ef5ff7883f780776b607
--- /dev/null
+++ b/front/vendor/vue-i18n-xgettext/extractor.js
@@ -0,0 +1,203 @@
+'use strict';
+
+Object.defineProperty(exports, "__esModule", {
+  value: true
+});
+
+var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };
+
+var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();
+
+var _cheerio = require('cheerio');
+
+var _cheerio2 = _interopRequireDefault(_cheerio);
+
+var _pofile = require('pofile');
+
+var _pofile2 = _interopRequireDefault(_pofile);
+
+function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
+
+function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
+var tRegexp = new RegExp('.*\\$t\\([\'\"\`](.*)[\'\"\`]\\).*', 'g');
+
+var Translation = function () {
+  function Translation(filename, lineNumber, msg) {
+    _classCallCheck(this, Translation);
+
+    this.filename = filename;
+    this.lineNumber = lineNumber;
+    this.msg = msg;
+  }
+
+  _createClass(Translation, [{
+    key: 'toPofileItem',
+    value: function toPofileItem() {
+      var item = new _pofile2.default.Item();
+      item.msgid = this.msg;
+      item.msgctxt = null;
+      item.references = [this.filename + ':' + this.lineNumber];
+      item.msgid_plural = null;
+      item.msgstr = [];
+      item.extractedComments = [];
+      return item;
+    }
+  }]);
+
+  return Translation;
+}();
+
+var Extractor = function () {
+  function Extractor(options) {
+    _classCallCheck(this, Extractor);
+
+    this.options = _extends({
+      startDelim: '{{',
+      endDelim: '}}',
+      attributes: ['path']
+    }, options);
+    this.translations = [];
+  }
+
+  _createClass(Extractor, [{
+    key: 'parse',
+    value: function parse(filename, content) {
+      var $ = _cheerio2.default.load(content, {
+        decodeEntities: false,
+        withStartIndices: true
+      });
+
+      var translations = $('template *').map(function (i, el) {
+        var node = $(el);
+        var msg = null;
+        if (node['0'].name === 'i18next') {
+          // here, we extract the translations from <i18next path="string">
+          msg = this.extractTranslationMessageFromI18Next(node);
+        }
+        if (msg) {
+          var truncatedText = content.substr(0, el.startIndex);
+          var lineNumber = truncatedText.split(/\r\n|\r|\n/).length;
+          return new Translation(filename, lineNumber, msg);
+        }
+      }.bind(this)).get();
+      var scriptTranslations = $('script,template').map(function (i, el) {
+        // here, we extract the translations from $t('string')
+        // within scripts and templates
+        var script = $(el).text();
+        var lines = script.split('\n');
+        var _translations = [];
+        lines.forEach(function (line) {
+          var truncatedText = content.substr(0, el.startIndex);
+          var matches;
+          while ((matches = tRegexp.exec(line)) !== null) {
+            var lineNumber = truncatedText.split(/\r\n|\r|\n/).length;
+            _translations.push(new Translation(filename, lineNumber, matches[1]));
+          }
+        })
+        return _translations
+      }.bind(this)).get();
+      this.translations = this.translations.concat(translations);
+      this.translations = this.translations.concat(scriptTranslations);
+    }
+  }, {
+    key: 'extractTranslationMessageFromI18Next',
+    value: function extractTranslationMessageFromI18Next(node) {
+      // extract from attributes
+      var _iteratorNormalCompletion = true;
+      var _didIteratorError = false;
+      var _iteratorError = undefined;
+
+      try {
+        for (var _iterator = this.options.attributes[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) {
+          var attr = _step.value;
+
+          if (node.attr('path')) {
+            return node.attr('path');
+          }
+        }
+      } catch (err) {
+        _didIteratorError = true;
+        _iteratorError = err;
+      } finally {
+        try {
+          if (!_iteratorNormalCompletion && _iterator.return) {
+            _iterator.return();
+          }
+        } finally {
+          if (_didIteratorError) {
+            throw _iteratorError;
+          }
+        }
+      }
+    }
+  }, {
+    key: 'toPofile',
+    value: function toPofile() {
+      var pofile = new _pofile2.default();
+      pofile.headers = {
+        'Last-Translator': 'vue-i18n-xgettext',
+        'Content-Type': 'text/plain; charset=UTF-8',
+        'Content-Transfer-Encoding': '8bit',
+        'MIME-Version': '1.1'
+      };
+
+      var itemMapping = {};
+      var _iteratorNormalCompletion2 = true;
+      var _didIteratorError2 = false;
+      var _iteratorError2 = undefined;
+
+      try {
+        for (var _iterator2 = this.translations[Symbol.iterator](), _step2; !(_iteratorNormalCompletion2 = (_step2 = _iterator2.next()).done); _iteratorNormalCompletion2 = true) {
+          var translation = _step2.value;
+
+          var _item = translation.toPofileItem();
+          if (!itemMapping[_item.msgid]) {
+            itemMapping[_item.msgid] = _item;
+          } else {
+            var oldItem = itemMapping[_item.msgid];
+            // TODO: deal with plurals/context
+            if (_item.references.length && oldItem.references.indexOf(_item.references[0]) === -1) {
+              oldItem.references.push(_item.references[0]);
+            }
+            if (_item.extractedComments.length && soldItem.extractedComments.indexOf(_item.extractedComments[0]) === -1) {
+              oldItem.extractedComments.push(_item.extractedComments[0]);
+            }
+          }
+        }
+      } catch (err) {
+        _didIteratorError2 = true;
+        _iteratorError2 = err;
+      } finally {
+        try {
+          if (!_iteratorNormalCompletion2 && _iterator2.return) {
+            _iterator2.return();
+          }
+        } finally {
+          if (_didIteratorError2) {
+            throw _iteratorError2;
+          }
+        }
+      }
+
+      for (var msgid in itemMapping) {
+        var item = itemMapping[msgid];
+        pofile.items.push(item);
+      }
+
+      pofile.items.sort(function (a, b) {
+        return a.msgid.localeCompare(b.msgid);
+      });
+      return pofile;
+    }
+  }, {
+    key: 'toString',
+    value: function toString() {
+      return this.toPofile().toString();
+    }
+  }]);
+
+  return Extractor;
+}();
+
+exports.default = Extractor;
+//# sourceMappingURL=extractor.js.map
diff --git a/front/vendor/vue-i18n-xgettext/index.js b/front/vendor/vue-i18n-xgettext/index.js
new file mode 100755
index 0000000000000000000000000000000000000000..2b8886e478253edf4461dc44570bd860a131738c
--- /dev/null
+++ b/front/vendor/vue-i18n-xgettext/index.js
@@ -0,0 +1,63 @@
+#!/usr/bin/env node
+'use strict';
+
+var _fs = require('fs');
+
+var _fs2 = _interopRequireDefault(_fs);
+
+var _minimist = require('minimist');
+
+var _minimist2 = _interopRequireDefault(_minimist);
+
+var _extractor = require('./extractor.js');
+
+var _extractor2 = _interopRequireDefault(_extractor);
+
+function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
+
+var argv = (0, _minimist2.default)(process.argv.slice(2));
+var files = argv._.sort() || [];
+var attributes = argv.attribute || [];
+var outputFile = argv.output || null;
+
+if (!files || files.length === 0) {
+  console.log('Usage: vue-i18n-xgettext [--attribute ATTRIBUTE] [--output OUTPUT_FILE] FILES');
+  process.exit(1);
+}
+
+var defaultAttributes = ['v-text'];
+var finalAttributes = defaultAttributes;
+if (typeof attributes === 'string') {
+  finalAttributes.push(attributes);
+} else {
+  finalAttributes = finalAttributes.concat(attributes);
+}
+
+var extractor = new _extractor2.default({
+  attributes: finalAttributes
+});
+
+files.forEach(function (filename) {
+  var extension = filename.split('.').pop();
+  if (extension !== 'vue') {
+    console.log('file ' + filename + ' with extension ' + extension + ' will not be processed (skipped)');
+    return;
+  }
+
+  var data = _fs2.default.readFileSync(filename, { encoding: 'utf-8' }).toString();
+
+  try {
+    extractor.parse(filename, data);
+  } catch (e) {
+    console.trace(e);
+    process.exit(1);
+  }
+});
+
+var output = extractor.toString();
+if (outputFile) {
+  _fs2.default.writeFileSync(outputFile, output);
+} else {
+  console.log(output);
+}
+//# sourceMappingURL=index.js.map
diff --git a/front/yarn.lock b/front/yarn.lock
new file mode 100644
index 0000000000000000000000000000000000000000..7cf3cb116a011e36dab7d92df31341c2da9eee1c
--- /dev/null
+++ b/front/yarn.lock
@@ -0,0 +1,7737 @@
+# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
+# yarn lockfile v1
+
+
+"@panter/vue-i18next@^0.9.1":
+  version "0.9.1"
+  resolved "https://registry.yarnpkg.com/@panter/vue-i18next/-/vue-i18next-0.9.1.tgz#a24694d37b7e2f54f67ac8a485f634203660e98d"
+  dependencies:
+    deepmerge "^2.0.0"
+
+abbrev@1:
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8"
+
+abbrev@1.0.x:
+  version "1.0.9"
+  resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.0.9.tgz#91b4792588a7738c25f35dd6f63752a2f8776135"
+
+accepts@1.3.3:
+  version "1.3.3"
+  resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.3.tgz#c3ca7434938648c3e0d9c1e328dd68b622c284ca"
+  dependencies:
+    mime-types "~2.1.11"
+    negotiator "0.6.1"
+
+accepts@~1.3.5:
+  version "1.3.5"
+  resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.5.tgz#eb777df6011723a3b14e8a72c0805c8e86746bd2"
+  dependencies:
+    mime-types "~2.1.18"
+    negotiator "0.6.1"
+
+acorn-dynamic-import@^2.0.0:
+  version "2.0.2"
+  resolved "https://registry.yarnpkg.com/acorn-dynamic-import/-/acorn-dynamic-import-2.0.2.tgz#c752bd210bef679501b6c6cb7fc84f8f47158cc4"
+  dependencies:
+    acorn "^4.0.3"
+
+acorn-jsx@^3.0.0:
+  version "3.0.1"
+  resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-3.0.1.tgz#afdf9488fb1ecefc8348f6fb22f464e32a58b36b"
+  dependencies:
+    acorn "^3.0.4"
+
+acorn@^3.0.4:
+  version "3.3.0"
+  resolved "https://registry.yarnpkg.com/acorn/-/acorn-3.3.0.tgz#45e37fb39e8da3f25baee3ff5369e2bb5f22017a"
+
+acorn@^4.0.3:
+  version "4.0.13"
+  resolved "https://registry.yarnpkg.com/acorn/-/acorn-4.0.13.tgz#105495ae5361d697bd195c825192e1ad7f253787"
+
+acorn@^5.0.0, acorn@^5.3.0, acorn@^5.5.0:
+  version "5.5.3"
+  resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.5.3.tgz#f473dd47e0277a08e28e9bec5aeeb04751f0b8c9"
+
+after@0.8.2:
+  version "0.8.2"
+  resolved "https://registry.yarnpkg.com/after/-/after-0.8.2.tgz#fedb394f9f0e02aa9768e702bda23b505fae7e1f"
+
+agent-base@2:
+  version "2.1.1"
+  resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-2.1.1.tgz#d6de10d5af6132d5bd692427d46fc538539094c7"
+  dependencies:
+    extend "~3.0.0"
+    semver "~5.0.1"
+
+ajv-keywords@^1.0.0:
+  version "1.5.1"
+  resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-1.5.1.tgz#314dd0a4b3368fad3dfcdc54ede6171b886daf3c"
+
+ajv-keywords@^3.1.0:
+  version "3.1.0"
+  resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.1.0.tgz#ac2b27939c543e95d2c06e7f7f5c27be4aa543be"
+
+ajv@^4.7.0, ajv@^4.9.1:
+  version "4.11.8"
+  resolved "https://registry.yarnpkg.com/ajv/-/ajv-4.11.8.tgz#82ffb02b29e662ae53bdc20af15947706739c536"
+  dependencies:
+    co "^4.6.0"
+    json-stable-stringify "^1.0.1"
+
+ajv@^5.0.0, ajv@^5.1.0:
+  version "5.5.2"
+  resolved "https://registry.yarnpkg.com/ajv/-/ajv-5.5.2.tgz#73b5eeca3fab653e3d3f9422b341ad42205dc965"
+  dependencies:
+    co "^4.6.0"
+    fast-deep-equal "^1.0.0"
+    fast-json-stable-stringify "^2.0.0"
+    json-schema-traverse "^0.3.0"
+
+ajv@^6.1.0:
+  version "6.4.0"
+  resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.4.0.tgz#d3aff78e9277549771daf0164cff48482b754fc6"
+  dependencies:
+    fast-deep-equal "^1.0.0"
+    fast-json-stable-stringify "^2.0.0"
+    json-schema-traverse "^0.3.0"
+    uri-js "^3.0.2"
+
+align-text@^0.1.1, align-text@^0.1.3:
+  version "0.1.4"
+  resolved "https://registry.yarnpkg.com/align-text/-/align-text-0.1.4.tgz#0cd90a561093f35d0a99256c22b7069433fad117"
+  dependencies:
+    kind-of "^3.0.2"
+    longest "^1.0.1"
+    repeat-string "^1.5.2"
+
+alphanum-sort@^1.0.1, alphanum-sort@^1.0.2:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/alphanum-sort/-/alphanum-sort-1.0.2.tgz#97a1119649b211ad33691d9f9f486a8ec9fbe0a3"
+
+amdefine@>=0.0.4:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/amdefine/-/amdefine-1.0.1.tgz#4a5282ac164729e93619bcfd3ad151f817ce91f5"
+
+ansi-escapes@^1.1.0:
+  version "1.4.0"
+  resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-1.4.0.tgz#d3a8a83b319aa67793662b13e761c7911422306e"
+
+ansi-html@0.0.7:
+  version "0.0.7"
+  resolved "https://registry.yarnpkg.com/ansi-html/-/ansi-html-0.0.7.tgz#813584021962a9e9e6fd039f940d12f56ca7859e"
+
+ansi-regex@^2.0.0:
+  version "2.1.1"
+  resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df"
+
+ansi-regex@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-3.0.0.tgz#ed0317c322064f79466c02966bddb605ab37d998"
+
+ansi-styles@^2.2.1:
+  version "2.2.1"
+  resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-2.2.1.tgz#b432dd3358b634cf75e1e4664368240533c1ddbe"
+
+ansi-styles@^3.2.1:
+  version "3.2.1"
+  resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d"
+  dependencies:
+    color-convert "^1.9.0"
+
+anymatch@^1.3.0:
+  version "1.3.2"
+  resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-1.3.2.tgz#553dcb8f91e3c889845dfdba34c77721b90b9d7a"
+  dependencies:
+    micromatch "^2.1.5"
+    normalize-path "^2.0.0"
+
+anymatch@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-2.0.0.tgz#bcb24b4f37934d9aa7ac17b4adaf89e7c76ef2eb"
+  dependencies:
+    micromatch "^3.1.4"
+    normalize-path "^2.1.1"
+
+aproba@^1.0.3, aproba@^1.1.1:
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/aproba/-/aproba-1.2.0.tgz#6802e6264efd18c790a1b0d517f0f2627bf2c94a"
+
+are-we-there-yet@~1.1.2:
+  version "1.1.4"
+  resolved "https://registry.yarnpkg.com/are-we-there-yet/-/are-we-there-yet-1.1.4.tgz#bb5dca382bb94f05e15194373d16fd3ba1ca110d"
+  dependencies:
+    delegates "^1.0.0"
+    readable-stream "^2.0.6"
+
+argparse@^1.0.7:
+  version "1.0.10"
+  resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911"
+  dependencies:
+    sprintf-js "~1.0.2"
+
+arr-diff@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/arr-diff/-/arr-diff-2.0.0.tgz#8f3b827f955a8bd669697e4a4256ac3ceae356cf"
+  dependencies:
+    arr-flatten "^1.0.1"
+
+arr-diff@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/arr-diff/-/arr-diff-4.0.0.tgz#d6461074febfec71e7e15235761a329a5dc7c520"
+
+arr-flatten@^1.0.1, arr-flatten@^1.1.0:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/arr-flatten/-/arr-flatten-1.1.0.tgz#36048bbff4e7b47e136644316c99669ea5ae91f1"
+
+arr-union@^3.1.0:
+  version "3.1.0"
+  resolved "https://registry.yarnpkg.com/arr-union/-/arr-union-3.1.0.tgz#e39b09aea9def866a8f206e288af63919bae39c4"
+
+array-find-index@^1.0.1:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/array-find-index/-/array-find-index-1.0.2.tgz#df010aa1287e164bbda6f9723b0a96a1ec4187a1"
+
+array-flatten@1.1.1:
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2"
+
+array-slice@^0.2.3:
+  version "0.2.3"
+  resolved "https://registry.yarnpkg.com/array-slice/-/array-slice-0.2.3.tgz#dd3cfb80ed7973a75117cdac69b0b99ec86186f5"
+
+array-union@^1.0.1:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/array-union/-/array-union-1.0.2.tgz#9a34410e4f4e3da23dea375be5be70f24778ec39"
+  dependencies:
+    array-uniq "^1.0.1"
+
+array-uniq@^1.0.1:
+  version "1.0.3"
+  resolved "https://registry.yarnpkg.com/array-uniq/-/array-uniq-1.0.3.tgz#af6ac877a25cc7f74e058894753858dfdb24fdb6"
+
+array-unique@^0.2.1:
+  version "0.2.1"
+  resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.2.1.tgz#a1d97ccafcbc2625cc70fadceb36a50c58b01a53"
+
+array-unique@^0.3.2:
+  version "0.3.2"
+  resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.3.2.tgz#a894b75d4bc4f6cd679ef3244a9fd8f46ae2d428"
+
+arraybuffer.slice@0.0.6:
+  version "0.0.6"
+  resolved "https://registry.yarnpkg.com/arraybuffer.slice/-/arraybuffer.slice-0.0.6.tgz#f33b2159f0532a3f3107a272c0ccfbd1ad2979ca"
+
+arrify@^1.0.0, arrify@^1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/arrify/-/arrify-1.0.1.tgz#898508da2226f380df904728456849c1501a4b0d"
+
+asn1.js@^4.0.0:
+  version "4.10.1"
+  resolved "https://registry.yarnpkg.com/asn1.js/-/asn1.js-4.10.1.tgz#b9c2bf5805f1e64aadeed6df3a2bfafb5a73f5a0"
+  dependencies:
+    bn.js "^4.0.0"
+    inherits "^2.0.1"
+    minimalistic-assert "^1.0.0"
+
+asn1@~0.2.3:
+  version "0.2.3"
+  resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.3.tgz#dac8787713c9966849fc8180777ebe9c1ddf3b86"
+
+assert-plus@1.0.0, assert-plus@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525"
+
+assert-plus@^0.2.0:
+  version "0.2.0"
+  resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-0.2.0.tgz#d74e1b87e7affc0db8aadb7021f3fe48101ab234"
+
+assert@^1.1.1:
+  version "1.4.1"
+  resolved "https://registry.yarnpkg.com/assert/-/assert-1.4.1.tgz#99912d591836b5a6f5b345c0f07eefc08fc65d91"
+  dependencies:
+    util "0.10.3"
+
+assertion-error@1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/assertion-error/-/assertion-error-1.0.0.tgz#c7f85438fdd466bc7ca16ab90c81513797a5d23b"
+
+assertion-error@^1.0.1:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/assertion-error/-/assertion-error-1.1.0.tgz#e60b6b0e8f301bd97e5375215bda406c85118c0b"
+
+assign-symbols@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/assign-symbols/-/assign-symbols-1.0.0.tgz#59667f41fadd4f20ccbc2bb96b8d4f7f78ec0367"
+
+ast-types@0.x.x:
+  version "0.11.3"
+  resolved "https://registry.yarnpkg.com/ast-types/-/ast-types-0.11.3.tgz#c20757fe72ee71278ea0ff3d87e5c2ca30d9edf8"
+
+async-each@^1.0.0:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/async-each/-/async-each-1.0.1.tgz#19d386a1d9edc6e7c1c85d388aedbcc56d33602d"
+
+async-foreach@^0.1.3:
+  version "0.1.3"
+  resolved "https://registry.yarnpkg.com/async-foreach/-/async-foreach-0.1.3.tgz#36121f845c0578172de419a97dbeb1d16ec34542"
+
+async-limiter@~1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/async-limiter/-/async-limiter-1.0.0.tgz#78faed8c3d074ab81f22b4e985d79e8738f720f8"
+
+async@1.x, async@^1.4.0:
+  version "1.5.2"
+  resolved "https://registry.yarnpkg.com/async/-/async-1.5.2.tgz#ec6a61ae56480c0c3cb241c95618e20892f9672a"
+
+async@^2.0.0, async@^2.1.2:
+  version "2.6.0"
+  resolved "https://registry.yarnpkg.com/async/-/async-2.6.0.tgz#61a29abb6fcc026fea77e56d1c6ec53a795951f4"
+  dependencies:
+    lodash "^4.14.0"
+
+asynckit@^0.4.0:
+  version "0.4.0"
+  resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79"
+
+atob@^2.0.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.0.tgz#ab2b150e51d7b122b9efc8d7340c06b6c41076bc"
+
+autoprefixer@^6.3.1, autoprefixer@^6.7.2:
+  version "6.7.7"
+  resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-6.7.7.tgz#1dbd1c835658e35ce3f9984099db00585c782014"
+  dependencies:
+    browserslist "^1.7.6"
+    caniuse-db "^1.0.30000634"
+    normalize-range "^0.1.2"
+    num2fraction "^1.2.2"
+    postcss "^5.2.16"
+    postcss-value-parser "^3.2.3"
+
+aws-sign2@~0.6.0:
+  version "0.6.0"
+  resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.6.0.tgz#14342dd38dbcc94d0e5b87d763cd63612c0e794f"
+
+aws-sign2@~0.7.0:
+  version "0.7.0"
+  resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8"
+
+aws4@^1.2.1, aws4@^1.6.0:
+  version "1.7.0"
+  resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.7.0.tgz#d4d0e9b9dbfca77bf08eeb0a8a471550fe39e289"
+
+axios@^0.17.1:
+  version "0.17.1"
+  resolved "https://registry.yarnpkg.com/axios/-/axios-0.17.1.tgz#2d8e3e5d0bdbd7327f91bc814f5c57660f81824d"
+  dependencies:
+    follow-redirects "^1.2.5"
+    is-buffer "^1.1.5"
+
+babel-code-frame@^6.16.0, babel-code-frame@^6.22.0, babel-code-frame@^6.26.0:
+  version "6.26.0"
+  resolved "https://registry.yarnpkg.com/babel-code-frame/-/babel-code-frame-6.26.0.tgz#63fd43f7dc1e3bb7ce35947db8fe369a3f58c74b"
+  dependencies:
+    chalk "^1.1.3"
+    esutils "^2.0.2"
+    js-tokens "^3.0.2"
+
+babel-core@^6.22.1, babel-core@^6.26.0, babel-core@~6:
+  version "6.26.0"
+  resolved "https://registry.yarnpkg.com/babel-core/-/babel-core-6.26.0.tgz#af32f78b31a6fcef119c87b0fd8d9753f03a0bb8"
+  dependencies:
+    babel-code-frame "^6.26.0"
+    babel-generator "^6.26.0"
+    babel-helpers "^6.24.1"
+    babel-messages "^6.23.0"
+    babel-register "^6.26.0"
+    babel-runtime "^6.26.0"
+    babel-template "^6.26.0"
+    babel-traverse "^6.26.0"
+    babel-types "^6.26.0"
+    babylon "^6.18.0"
+    convert-source-map "^1.5.0"
+    debug "^2.6.8"
+    json5 "^0.5.1"
+    lodash "^4.17.4"
+    minimatch "^3.0.4"
+    path-is-absolute "^1.0.1"
+    private "^0.1.7"
+    slash "^1.0.0"
+    source-map "^0.5.6"
+
+babel-eslint@^7.1.1:
+  version "7.2.3"
+  resolved "https://registry.yarnpkg.com/babel-eslint/-/babel-eslint-7.2.3.tgz#b2fe2d80126470f5c19442dc757253a897710827"
+  dependencies:
+    babel-code-frame "^6.22.0"
+    babel-traverse "^6.23.1"
+    babel-types "^6.23.0"
+    babylon "^6.17.0"
+
+babel-generator@^6.18.0, babel-generator@^6.26.0:
+  version "6.26.1"
+  resolved "https://registry.yarnpkg.com/babel-generator/-/babel-generator-6.26.1.tgz#1844408d3b8f0d35a404ea7ac180f087a601bd90"
+  dependencies:
+    babel-messages "^6.23.0"
+    babel-runtime "^6.26.0"
+    babel-types "^6.26.0"
+    detect-indent "^4.0.0"
+    jsesc "^1.3.0"
+    lodash "^4.17.4"
+    source-map "^0.5.7"
+    trim-right "^1.0.1"
+
+babel-helper-bindify-decorators@^6.24.1:
+  version "6.24.1"
+  resolved "https://registry.yarnpkg.com/babel-helper-bindify-decorators/-/babel-helper-bindify-decorators-6.24.1.tgz#14c19e5f142d7b47f19a52431e52b1ccbc40a330"
+  dependencies:
+    babel-runtime "^6.22.0"
+    babel-traverse "^6.24.1"
+    babel-types "^6.24.1"
+
+babel-helper-builder-binary-assignment-operator-visitor@^6.24.1:
+  version "6.24.1"
+  resolved "https://registry.yarnpkg.com/babel-helper-builder-binary-assignment-operator-visitor/-/babel-helper-builder-binary-assignment-operator-visitor-6.24.1.tgz#cce4517ada356f4220bcae8a02c2b346f9a56664"
+  dependencies:
+    babel-helper-explode-assignable-expression "^6.24.1"
+    babel-runtime "^6.22.0"
+    babel-types "^6.24.1"
+
+babel-helper-call-delegate@^6.24.1:
+  version "6.24.1"
+  resolved "https://registry.yarnpkg.com/babel-helper-call-delegate/-/babel-helper-call-delegate-6.24.1.tgz#ece6aacddc76e41c3461f88bfc575bd0daa2df8d"
+  dependencies:
+    babel-helper-hoist-variables "^6.24.1"
+    babel-runtime "^6.22.0"
+    babel-traverse "^6.24.1"
+    babel-types "^6.24.1"
+
+babel-helper-define-map@^6.24.1:
+  version "6.26.0"
+  resolved "https://registry.yarnpkg.com/babel-helper-define-map/-/babel-helper-define-map-6.26.0.tgz#a5f56dab41a25f97ecb498c7ebaca9819f95be5f"
+  dependencies:
+    babel-helper-function-name "^6.24.1"
+    babel-runtime "^6.26.0"
+    babel-types "^6.26.0"
+    lodash "^4.17.4"
+
+babel-helper-explode-assignable-expression@^6.24.1:
+  version "6.24.1"
+  resolved "https://registry.yarnpkg.com/babel-helper-explode-assignable-expression/-/babel-helper-explode-assignable-expression-6.24.1.tgz#f25b82cf7dc10433c55f70592d5746400ac22caa"
+  dependencies:
+    babel-runtime "^6.22.0"
+    babel-traverse "^6.24.1"
+    babel-types "^6.24.1"
+
+babel-helper-explode-class@^6.24.1:
+  version "6.24.1"
+  resolved "https://registry.yarnpkg.com/babel-helper-explode-class/-/babel-helper-explode-class-6.24.1.tgz#7dc2a3910dee007056e1e31d640ced3d54eaa9eb"
+  dependencies:
+    babel-helper-bindify-decorators "^6.24.1"
+    babel-runtime "^6.22.0"
+    babel-traverse "^6.24.1"
+    babel-types "^6.24.1"
+
+babel-helper-function-name@^6.24.1:
+  version "6.24.1"
+  resolved "https://registry.yarnpkg.com/babel-helper-function-name/-/babel-helper-function-name-6.24.1.tgz#d3475b8c03ed98242a25b48351ab18399d3580a9"
+  dependencies:
+    babel-helper-get-function-arity "^6.24.1"
+    babel-runtime "^6.22.0"
+    babel-template "^6.24.1"
+    babel-traverse "^6.24.1"
+    babel-types "^6.24.1"
+
+babel-helper-get-function-arity@^6.24.1:
+  version "6.24.1"
+  resolved "https://registry.yarnpkg.com/babel-helper-get-function-arity/-/babel-helper-get-function-arity-6.24.1.tgz#8f7782aa93407c41d3aa50908f89b031b1b6853d"
+  dependencies:
+    babel-runtime "^6.22.0"
+    babel-types "^6.24.1"
+
+babel-helper-hoist-variables@^6.24.1:
+  version "6.24.1"
+  resolved "https://registry.yarnpkg.com/babel-helper-hoist-variables/-/babel-helper-hoist-variables-6.24.1.tgz#1ecb27689c9d25513eadbc9914a73f5408be7a76"
+  dependencies:
+    babel-runtime "^6.22.0"
+    babel-types "^6.24.1"
+
+babel-helper-optimise-call-expression@^6.24.1:
+  version "6.24.1"
+  resolved "https://registry.yarnpkg.com/babel-helper-optimise-call-expression/-/babel-helper-optimise-call-expression-6.24.1.tgz#f7a13427ba9f73f8f4fa993c54a97882d1244257"
+  dependencies:
+    babel-runtime "^6.22.0"
+    babel-types "^6.24.1"
+
+babel-helper-regex@^6.24.1:
+  version "6.26.0"
+  resolved "https://registry.yarnpkg.com/babel-helper-regex/-/babel-helper-regex-6.26.0.tgz#325c59f902f82f24b74faceed0363954f6495e72"
+  dependencies:
+    babel-runtime "^6.26.0"
+    babel-types "^6.26.0"
+    lodash "^4.17.4"
+
+babel-helper-remap-async-to-generator@^6.24.1:
+  version "6.24.1"
+  resolved "https://registry.yarnpkg.com/babel-helper-remap-async-to-generator/-/babel-helper-remap-async-to-generator-6.24.1.tgz#5ec581827ad723fecdd381f1c928390676e4551b"
+  dependencies:
+    babel-helper-function-name "^6.24.1"
+    babel-runtime "^6.22.0"
+    babel-template "^6.24.1"
+    babel-traverse "^6.24.1"
+    babel-types "^6.24.1"
+
+babel-helper-replace-supers@^6.24.1:
+  version "6.24.1"
+  resolved "https://registry.yarnpkg.com/babel-helper-replace-supers/-/babel-helper-replace-supers-6.24.1.tgz#bf6dbfe43938d17369a213ca8a8bf74b6a90ab1a"
+  dependencies:
+    babel-helper-optimise-call-expression "^6.24.1"
+    babel-messages "^6.23.0"
+    babel-runtime "^6.22.0"
+    babel-template "^6.24.1"
+    babel-traverse "^6.24.1"
+    babel-types "^6.24.1"
+
+babel-helpers@^6.24.1:
+  version "6.24.1"
+  resolved "https://registry.yarnpkg.com/babel-helpers/-/babel-helpers-6.24.1.tgz#3471de9caec388e5c850e597e58a26ddf37602b2"
+  dependencies:
+    babel-runtime "^6.22.0"
+    babel-template "^6.24.1"
+
+babel-loader@7:
+  version "7.1.4"
+  resolved "https://registry.yarnpkg.com/babel-loader/-/babel-loader-7.1.4.tgz#e3463938bd4e6d55d1c174c5485d406a188ed015"
+  dependencies:
+    find-cache-dir "^1.0.0"
+    loader-utils "^1.0.2"
+    mkdirp "^0.5.1"
+
+babel-messages@^6.23.0:
+  version "6.23.0"
+  resolved "https://registry.yarnpkg.com/babel-messages/-/babel-messages-6.23.0.tgz#f3cdf4703858035b2a2951c6ec5edf6c62f2630e"
+  dependencies:
+    babel-runtime "^6.22.0"
+
+babel-plugin-check-es2015-constants@^6.22.0:
+  version "6.22.0"
+  resolved "https://registry.yarnpkg.com/babel-plugin-check-es2015-constants/-/babel-plugin-check-es2015-constants-6.22.0.tgz#35157b101426fd2ffd3da3f75c7d1e91835bbf8a"
+  dependencies:
+    babel-runtime "^6.22.0"
+
+babel-plugin-istanbul@^4.1.1:
+  version "4.1.6"
+  resolved "https://registry.yarnpkg.com/babel-plugin-istanbul/-/babel-plugin-istanbul-4.1.6.tgz#36c59b2192efce81c5b378321b74175add1c9a45"
+  dependencies:
+    babel-plugin-syntax-object-rest-spread "^6.13.0"
+    find-up "^2.1.0"
+    istanbul-lib-instrument "^1.10.1"
+    test-exclude "^4.2.1"
+
+babel-plugin-syntax-async-functions@^6.8.0:
+  version "6.13.0"
+  resolved "https://registry.yarnpkg.com/babel-plugin-syntax-async-functions/-/babel-plugin-syntax-async-functions-6.13.0.tgz#cad9cad1191b5ad634bf30ae0872391e0647be95"
+
+babel-plugin-syntax-async-generators@^6.5.0:
+  version "6.13.0"
+  resolved "https://registry.yarnpkg.com/babel-plugin-syntax-async-generators/-/babel-plugin-syntax-async-generators-6.13.0.tgz#6bc963ebb16eccbae6b92b596eb7f35c342a8b9a"
+
+babel-plugin-syntax-class-properties@^6.8.0:
+  version "6.13.0"
+  resolved "https://registry.yarnpkg.com/babel-plugin-syntax-class-properties/-/babel-plugin-syntax-class-properties-6.13.0.tgz#d7eb23b79a317f8543962c505b827c7d6cac27de"
+
+babel-plugin-syntax-decorators@^6.13.0:
+  version "6.13.0"
+  resolved "https://registry.yarnpkg.com/babel-plugin-syntax-decorators/-/babel-plugin-syntax-decorators-6.13.0.tgz#312563b4dbde3cc806cee3e416cceeaddd11ac0b"
+
+babel-plugin-syntax-dynamic-import@^6.18.0:
+  version "6.18.0"
+  resolved "https://registry.yarnpkg.com/babel-plugin-syntax-dynamic-import/-/babel-plugin-syntax-dynamic-import-6.18.0.tgz#8d6a26229c83745a9982a441051572caa179b1da"
+
+babel-plugin-syntax-exponentiation-operator@^6.8.0:
+  version "6.13.0"
+  resolved "https://registry.yarnpkg.com/babel-plugin-syntax-exponentiation-operator/-/babel-plugin-syntax-exponentiation-operator-6.13.0.tgz#9ee7e8337290da95288201a6a57f4170317830de"
+
+babel-plugin-syntax-object-rest-spread@^6.13.0, babel-plugin-syntax-object-rest-spread@^6.8.0:
+  version "6.13.0"
+  resolved "https://registry.yarnpkg.com/babel-plugin-syntax-object-rest-spread/-/babel-plugin-syntax-object-rest-spread-6.13.0.tgz#fd6536f2bce13836ffa3a5458c4903a597bb3bf5"
+
+babel-plugin-syntax-trailing-function-commas@^6.22.0:
+  version "6.22.0"
+  resolved "https://registry.yarnpkg.com/babel-plugin-syntax-trailing-function-commas/-/babel-plugin-syntax-trailing-function-commas-6.22.0.tgz#ba0360937f8d06e40180a43fe0d5616fff532cf3"
+
+babel-plugin-transform-async-generator-functions@^6.24.1:
+  version "6.24.1"
+  resolved "https://registry.yarnpkg.com/babel-plugin-transform-async-generator-functions/-/babel-plugin-transform-async-generator-functions-6.24.1.tgz#f058900145fd3e9907a6ddf28da59f215258a5db"
+  dependencies:
+    babel-helper-remap-async-to-generator "^6.24.1"
+    babel-plugin-syntax-async-generators "^6.5.0"
+    babel-runtime "^6.22.0"
+
+babel-plugin-transform-async-to-generator@^6.22.0, babel-plugin-transform-async-to-generator@^6.24.1:
+  version "6.24.1"
+  resolved "https://registry.yarnpkg.com/babel-plugin-transform-async-to-generator/-/babel-plugin-transform-async-to-generator-6.24.1.tgz#6536e378aff6cb1d5517ac0e40eb3e9fc8d08761"
+  dependencies:
+    babel-helper-remap-async-to-generator "^6.24.1"
+    babel-plugin-syntax-async-functions "^6.8.0"
+    babel-runtime "^6.22.0"
+
+babel-plugin-transform-class-properties@^6.24.1:
+  version "6.24.1"
+  resolved "https://registry.yarnpkg.com/babel-plugin-transform-class-properties/-/babel-plugin-transform-class-properties-6.24.1.tgz#6a79763ea61d33d36f37b611aa9def81a81b46ac"
+  dependencies:
+    babel-helper-function-name "^6.24.1"
+    babel-plugin-syntax-class-properties "^6.8.0"
+    babel-runtime "^6.22.0"
+    babel-template "^6.24.1"
+
+babel-plugin-transform-decorators@^6.24.1:
+  version "6.24.1"
+  resolved "https://registry.yarnpkg.com/babel-plugin-transform-decorators/-/babel-plugin-transform-decorators-6.24.1.tgz#788013d8f8c6b5222bdf7b344390dfd77569e24d"
+  dependencies:
+    babel-helper-explode-class "^6.24.1"
+    babel-plugin-syntax-decorators "^6.13.0"
+    babel-runtime "^6.22.0"
+    babel-template "^6.24.1"
+    babel-types "^6.24.1"
+
+babel-plugin-transform-es2015-arrow-functions@^6.22.0:
+  version "6.22.0"
+  resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-arrow-functions/-/babel-plugin-transform-es2015-arrow-functions-6.22.0.tgz#452692cb711d5f79dc7f85e440ce41b9f244d221"
+  dependencies:
+    babel-runtime "^6.22.0"
+
+babel-plugin-transform-es2015-block-scoped-functions@^6.22.0:
+  version "6.22.0"
+  resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-block-scoped-functions/-/babel-plugin-transform-es2015-block-scoped-functions-6.22.0.tgz#bbc51b49f964d70cb8d8e0b94e820246ce3a6141"
+  dependencies:
+    babel-runtime "^6.22.0"
+
+babel-plugin-transform-es2015-block-scoping@^6.23.0:
+  version "6.26.0"
+  resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-block-scoping/-/babel-plugin-transform-es2015-block-scoping-6.26.0.tgz#d70f5299c1308d05c12f463813b0a09e73b1895f"
+  dependencies:
+    babel-runtime "^6.26.0"
+    babel-template "^6.26.0"
+    babel-traverse "^6.26.0"
+    babel-types "^6.26.0"
+    lodash "^4.17.4"
+
+babel-plugin-transform-es2015-classes@^6.23.0:
+  version "6.24.1"
+  resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-classes/-/babel-plugin-transform-es2015-classes-6.24.1.tgz#5a4c58a50c9c9461e564b4b2a3bfabc97a2584db"
+  dependencies:
+    babel-helper-define-map "^6.24.1"
+    babel-helper-function-name "^6.24.1"
+    babel-helper-optimise-call-expression "^6.24.1"
+    babel-helper-replace-supers "^6.24.1"
+    babel-messages "^6.23.0"
+    babel-runtime "^6.22.0"
+    babel-template "^6.24.1"
+    babel-traverse "^6.24.1"
+    babel-types "^6.24.1"
+
+babel-plugin-transform-es2015-computed-properties@^6.22.0:
+  version "6.24.1"
+  resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-computed-properties/-/babel-plugin-transform-es2015-computed-properties-6.24.1.tgz#6fe2a8d16895d5634f4cd999b6d3480a308159b3"
+  dependencies:
+    babel-runtime "^6.22.0"
+    babel-template "^6.24.1"
+
+babel-plugin-transform-es2015-destructuring@^6.23.0:
+  version "6.23.0"
+  resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-destructuring/-/babel-plugin-transform-es2015-destructuring-6.23.0.tgz#997bb1f1ab967f682d2b0876fe358d60e765c56d"
+  dependencies:
+    babel-runtime "^6.22.0"
+
+babel-plugin-transform-es2015-duplicate-keys@^6.22.0:
+  version "6.24.1"
+  resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-duplicate-keys/-/babel-plugin-transform-es2015-duplicate-keys-6.24.1.tgz#73eb3d310ca969e3ef9ec91c53741a6f1576423e"
+  dependencies:
+    babel-runtime "^6.22.0"
+    babel-types "^6.24.1"
+
+babel-plugin-transform-es2015-for-of@^6.23.0:
+  version "6.23.0"
+  resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-for-of/-/babel-plugin-transform-es2015-for-of-6.23.0.tgz#f47c95b2b613df1d3ecc2fdb7573623c75248691"
+  dependencies:
+    babel-runtime "^6.22.0"
+
+babel-plugin-transform-es2015-function-name@^6.22.0:
+  version "6.24.1"
+  resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-function-name/-/babel-plugin-transform-es2015-function-name-6.24.1.tgz#834c89853bc36b1af0f3a4c5dbaa94fd8eacaa8b"
+  dependencies:
+    babel-helper-function-name "^6.24.1"
+    babel-runtime "^6.22.0"
+    babel-types "^6.24.1"
+
+babel-plugin-transform-es2015-literals@^6.22.0:
+  version "6.22.0"
+  resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-literals/-/babel-plugin-transform-es2015-literals-6.22.0.tgz#4f54a02d6cd66cf915280019a31d31925377ca2e"
+  dependencies:
+    babel-runtime "^6.22.0"
+
+babel-plugin-transform-es2015-modules-amd@^6.22.0, babel-plugin-transform-es2015-modules-amd@^6.24.1:
+  version "6.24.1"
+  resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-modules-amd/-/babel-plugin-transform-es2015-modules-amd-6.24.1.tgz#3b3e54017239842d6d19c3011c4bd2f00a00d154"
+  dependencies:
+    babel-plugin-transform-es2015-modules-commonjs "^6.24.1"
+    babel-runtime "^6.22.0"
+    babel-template "^6.24.1"
+
+babel-plugin-transform-es2015-modules-commonjs@^6.23.0, babel-plugin-transform-es2015-modules-commonjs@^6.24.1:
+  version "6.26.0"
+  resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-modules-commonjs/-/babel-plugin-transform-es2015-modules-commonjs-6.26.0.tgz#0d8394029b7dc6abe1a97ef181e00758dd2e5d8a"
+  dependencies:
+    babel-plugin-transform-strict-mode "^6.24.1"
+    babel-runtime "^6.26.0"
+    babel-template "^6.26.0"
+    babel-types "^6.26.0"
+
+babel-plugin-transform-es2015-modules-systemjs@^6.23.0:
+  version "6.24.1"
+  resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-modules-systemjs/-/babel-plugin-transform-es2015-modules-systemjs-6.24.1.tgz#ff89a142b9119a906195f5f106ecf305d9407d23"
+  dependencies:
+    babel-helper-hoist-variables "^6.24.1"
+    babel-runtime "^6.22.0"
+    babel-template "^6.24.1"
+
+babel-plugin-transform-es2015-modules-umd@^6.23.0:
+  version "6.24.1"
+  resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-modules-umd/-/babel-plugin-transform-es2015-modules-umd-6.24.1.tgz#ac997e6285cd18ed6176adb607d602344ad38468"
+  dependencies:
+    babel-plugin-transform-es2015-modules-amd "^6.24.1"
+    babel-runtime "^6.22.0"
+    babel-template "^6.24.1"
+
+babel-plugin-transform-es2015-object-super@^6.22.0:
+  version "6.24.1"
+  resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-object-super/-/babel-plugin-transform-es2015-object-super-6.24.1.tgz#24cef69ae21cb83a7f8603dad021f572eb278f8d"
+  dependencies:
+    babel-helper-replace-supers "^6.24.1"
+    babel-runtime "^6.22.0"
+
+babel-plugin-transform-es2015-parameters@^6.23.0:
+  version "6.24.1"
+  resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-parameters/-/babel-plugin-transform-es2015-parameters-6.24.1.tgz#57ac351ab49caf14a97cd13b09f66fdf0a625f2b"
+  dependencies:
+    babel-helper-call-delegate "^6.24.1"
+    babel-helper-get-function-arity "^6.24.1"
+    babel-runtime "^6.22.0"
+    babel-template "^6.24.1"
+    babel-traverse "^6.24.1"
+    babel-types "^6.24.1"
+
+babel-plugin-transform-es2015-shorthand-properties@^6.22.0:
+  version "6.24.1"
+  resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-shorthand-properties/-/babel-plugin-transform-es2015-shorthand-properties-6.24.1.tgz#24f875d6721c87661bbd99a4622e51f14de38aa0"
+  dependencies:
+    babel-runtime "^6.22.0"
+    babel-types "^6.24.1"
+
+babel-plugin-transform-es2015-spread@^6.22.0:
+  version "6.22.0"
+  resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-spread/-/babel-plugin-transform-es2015-spread-6.22.0.tgz#d6d68a99f89aedc4536c81a542e8dd9f1746f8d1"
+  dependencies:
+    babel-runtime "^6.22.0"
+
+babel-plugin-transform-es2015-sticky-regex@^6.22.0:
+  version "6.24.1"
+  resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-sticky-regex/-/babel-plugin-transform-es2015-sticky-regex-6.24.1.tgz#00c1cdb1aca71112cdf0cf6126c2ed6b457ccdbc"
+  dependencies:
+    babel-helper-regex "^6.24.1"
+    babel-runtime "^6.22.0"
+    babel-types "^6.24.1"
+
+babel-plugin-transform-es2015-template-literals@^6.22.0:
+  version "6.22.0"
+  resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-template-literals/-/babel-plugin-transform-es2015-template-literals-6.22.0.tgz#a84b3450f7e9f8f1f6839d6d687da84bb1236d8d"
+  dependencies:
+    babel-runtime "^6.22.0"
+
+babel-plugin-transform-es2015-typeof-symbol@^6.23.0:
+  version "6.23.0"
+  resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-typeof-symbol/-/babel-plugin-transform-es2015-typeof-symbol-6.23.0.tgz#dec09f1cddff94b52ac73d505c84df59dcceb372"
+  dependencies:
+    babel-runtime "^6.22.0"
+
+babel-plugin-transform-es2015-unicode-regex@^6.22.0:
+  version "6.24.1"
+  resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-unicode-regex/-/babel-plugin-transform-es2015-unicode-regex-6.24.1.tgz#d38b12f42ea7323f729387f18a7c5ae1faeb35e9"
+  dependencies:
+    babel-helper-regex "^6.24.1"
+    babel-runtime "^6.22.0"
+    regexpu-core "^2.0.0"
+
+babel-plugin-transform-exponentiation-operator@^6.22.0, babel-plugin-transform-exponentiation-operator@^6.24.1:
+  version "6.24.1"
+  resolved "https://registry.yarnpkg.com/babel-plugin-transform-exponentiation-operator/-/babel-plugin-transform-exponentiation-operator-6.24.1.tgz#2ab0c9c7f3098fa48907772bb813fe41e8de3a0e"
+  dependencies:
+    babel-helper-builder-binary-assignment-operator-visitor "^6.24.1"
+    babel-plugin-syntax-exponentiation-operator "^6.8.0"
+    babel-runtime "^6.22.0"
+
+babel-plugin-transform-object-rest-spread@^6.22.0:
+  version "6.26.0"
+  resolved "https://registry.yarnpkg.com/babel-plugin-transform-object-rest-spread/-/babel-plugin-transform-object-rest-spread-6.26.0.tgz#0f36692d50fef6b7e2d4b3ac1478137a963b7b06"
+  dependencies:
+    babel-plugin-syntax-object-rest-spread "^6.8.0"
+    babel-runtime "^6.26.0"
+
+babel-plugin-transform-regenerator@^6.22.0:
+  version "6.26.0"
+  resolved "https://registry.yarnpkg.com/babel-plugin-transform-regenerator/-/babel-plugin-transform-regenerator-6.26.0.tgz#e0703696fbde27f0a3efcacf8b4dca2f7b3a8f2f"
+  dependencies:
+    regenerator-transform "^0.10.0"
+
+babel-plugin-transform-runtime@^6.22.0:
+  version "6.23.0"
+  resolved "https://registry.yarnpkg.com/babel-plugin-transform-runtime/-/babel-plugin-transform-runtime-6.23.0.tgz#88490d446502ea9b8e7efb0fe09ec4d99479b1ee"
+  dependencies:
+    babel-runtime "^6.22.0"
+
+babel-plugin-transform-strict-mode@^6.24.1:
+  version "6.24.1"
+  resolved "https://registry.yarnpkg.com/babel-plugin-transform-strict-mode/-/babel-plugin-transform-strict-mode-6.24.1.tgz#d5faf7aa578a65bbe591cf5edae04a0c67020758"
+  dependencies:
+    babel-runtime "^6.22.0"
+    babel-types "^6.24.1"
+
+babel-preset-env@^1.3.2:
+  version "1.6.1"
+  resolved "https://registry.yarnpkg.com/babel-preset-env/-/babel-preset-env-1.6.1.tgz#a18b564cc9b9afdf4aae57ae3c1b0d99188e6f48"
+  dependencies:
+    babel-plugin-check-es2015-constants "^6.22.0"
+    babel-plugin-syntax-trailing-function-commas "^6.22.0"
+    babel-plugin-transform-async-to-generator "^6.22.0"
+    babel-plugin-transform-es2015-arrow-functions "^6.22.0"
+    babel-plugin-transform-es2015-block-scoped-functions "^6.22.0"
+    babel-plugin-transform-es2015-block-scoping "^6.23.0"
+    babel-plugin-transform-es2015-classes "^6.23.0"
+    babel-plugin-transform-es2015-computed-properties "^6.22.0"
+    babel-plugin-transform-es2015-destructuring "^6.23.0"
+    babel-plugin-transform-es2015-duplicate-keys "^6.22.0"
+    babel-plugin-transform-es2015-for-of "^6.23.0"
+    babel-plugin-transform-es2015-function-name "^6.22.0"
+    babel-plugin-transform-es2015-literals "^6.22.0"
+    babel-plugin-transform-es2015-modules-amd "^6.22.0"
+    babel-plugin-transform-es2015-modules-commonjs "^6.23.0"
+    babel-plugin-transform-es2015-modules-systemjs "^6.23.0"
+    babel-plugin-transform-es2015-modules-umd "^6.23.0"
+    babel-plugin-transform-es2015-object-super "^6.22.0"
+    babel-plugin-transform-es2015-parameters "^6.23.0"
+    babel-plugin-transform-es2015-shorthand-properties "^6.22.0"
+    babel-plugin-transform-es2015-spread "^6.22.0"
+    babel-plugin-transform-es2015-sticky-regex "^6.22.0"
+    babel-plugin-transform-es2015-template-literals "^6.22.0"
+    babel-plugin-transform-es2015-typeof-symbol "^6.23.0"
+    babel-plugin-transform-es2015-unicode-regex "^6.22.0"
+    babel-plugin-transform-exponentiation-operator "^6.22.0"
+    babel-plugin-transform-regenerator "^6.22.0"
+    browserslist "^2.1.2"
+    invariant "^2.2.2"
+    semver "^5.3.0"
+
+babel-preset-stage-2@^6.22.0:
+  version "6.24.1"
+  resolved "https://registry.yarnpkg.com/babel-preset-stage-2/-/babel-preset-stage-2-6.24.1.tgz#d9e2960fb3d71187f0e64eec62bc07767219bdc1"
+  dependencies:
+    babel-plugin-syntax-dynamic-import "^6.18.0"
+    babel-plugin-transform-class-properties "^6.24.1"
+    babel-plugin-transform-decorators "^6.24.1"
+    babel-preset-stage-3 "^6.24.1"
+
+babel-preset-stage-3@^6.24.1:
+  version "6.24.1"
+  resolved "https://registry.yarnpkg.com/babel-preset-stage-3/-/babel-preset-stage-3-6.24.1.tgz#836ada0a9e7a7fa37cb138fb9326f87934a48395"
+  dependencies:
+    babel-plugin-syntax-trailing-function-commas "^6.22.0"
+    babel-plugin-transform-async-generator-functions "^6.24.1"
+    babel-plugin-transform-async-to-generator "^6.24.1"
+    babel-plugin-transform-exponentiation-operator "^6.24.1"
+    babel-plugin-transform-object-rest-spread "^6.22.0"
+
+babel-register@^6.22.0, babel-register@^6.26.0:
+  version "6.26.0"
+  resolved "https://registry.yarnpkg.com/babel-register/-/babel-register-6.26.0.tgz#6ed021173e2fcb486d7acb45c6009a856f647071"
+  dependencies:
+    babel-core "^6.26.0"
+    babel-runtime "^6.26.0"
+    core-js "^2.5.0"
+    home-or-tmp "^2.0.0"
+    lodash "^4.17.4"
+    mkdirp "^0.5.1"
+    source-map-support "^0.4.15"
+
+babel-runtime@^6.0.0, babel-runtime@^6.18.0, babel-runtime@^6.22.0, babel-runtime@^6.26.0:
+  version "6.26.0"
+  resolved "https://registry.yarnpkg.com/babel-runtime/-/babel-runtime-6.26.0.tgz#965c7058668e82b55d7bfe04ff2337bc8b5647fe"
+  dependencies:
+    core-js "^2.4.0"
+    regenerator-runtime "^0.11.0"
+
+babel-template@^6.16.0, babel-template@^6.24.1, babel-template@^6.26.0:
+  version "6.26.0"
+  resolved "https://registry.yarnpkg.com/babel-template/-/babel-template-6.26.0.tgz#de03e2d16396b069f46dd9fff8521fb1a0e35e02"
+  dependencies:
+    babel-runtime "^6.26.0"
+    babel-traverse "^6.26.0"
+    babel-types "^6.26.0"
+    babylon "^6.18.0"
+    lodash "^4.17.4"
+
+babel-traverse@^6.18.0, babel-traverse@^6.23.1, babel-traverse@^6.24.1, babel-traverse@^6.26.0:
+  version "6.26.0"
+  resolved "https://registry.yarnpkg.com/babel-traverse/-/babel-traverse-6.26.0.tgz#46a9cbd7edcc62c8e5c064e2d2d8d0f4035766ee"
+  dependencies:
+    babel-code-frame "^6.26.0"
+    babel-messages "^6.23.0"
+    babel-runtime "^6.26.0"
+    babel-types "^6.26.0"
+    babylon "^6.18.0"
+    debug "^2.6.8"
+    globals "^9.18.0"
+    invariant "^2.2.2"
+    lodash "^4.17.4"
+
+babel-types@^6.18.0, babel-types@^6.19.0, babel-types@^6.23.0, babel-types@^6.24.1, babel-types@^6.26.0:
+  version "6.26.0"
+  resolved "https://registry.yarnpkg.com/babel-types/-/babel-types-6.26.0.tgz#a3b073f94ab49eb6fa55cd65227a334380632497"
+  dependencies:
+    babel-runtime "^6.26.0"
+    esutils "^2.0.2"
+    lodash "^4.17.4"
+    to-fast-properties "^1.0.3"
+
+babylon@^6.17.0, babylon@^6.18.0:
+  version "6.18.0"
+  resolved "https://registry.yarnpkg.com/babylon/-/babylon-6.18.0.tgz#af2f3b88fa6f5c1e4c634d1a0f8eac4f55b395e3"
+
+backo2@1.0.2:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/backo2/-/backo2-1.0.2.tgz#31ab1ac8b129363463e35b3ebb69f4dfcfba7947"
+
+balanced-match@^0.4.2:
+  version "0.4.2"
+  resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-0.4.2.tgz#cb3f3e3c732dc0f01ee70b403f302e61d7709838"
+
+balanced-match@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767"
+
+base64-arraybuffer@0.1.5:
+  version "0.1.5"
+  resolved "https://registry.yarnpkg.com/base64-arraybuffer/-/base64-arraybuffer-0.1.5.tgz#73926771923b5a19747ad666aa5cd4bf9c6e9ce8"
+
+base64-js@^1.0.2:
+  version "1.2.3"
+  resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.2.3.tgz#fb13668233d9614cf5fb4bce95a9ba4096cdf801"
+
+base64id@1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/base64id/-/base64id-1.0.0.tgz#47688cb99bb6804f0e06d3e763b1c32e57d8e6b6"
+
+base@^0.11.1:
+  version "0.11.2"
+  resolved "https://registry.yarnpkg.com/base/-/base-0.11.2.tgz#7bde5ced145b6d551a90db87f83c558b4eb48a8f"
+  dependencies:
+    cache-base "^1.0.1"
+    class-utils "^0.3.5"
+    component-emitter "^1.2.1"
+    define-property "^1.0.0"
+    isobject "^3.0.1"
+    mixin-deep "^1.2.0"
+    pascalcase "^0.1.1"
+
+bcrypt-pbkdf@^1.0.0:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.1.tgz#63bc5dcb61331b92bc05fd528953c33462a06f8d"
+  dependencies:
+    tweetnacl "^0.14.3"
+
+better-assert@~1.0.0:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/better-assert/-/better-assert-1.0.2.tgz#40866b9e1b9e0b55b481894311e68faffaebc522"
+  dependencies:
+    callsite "1.0.0"
+
+bfj-node4@^5.2.0:
+  version "5.3.1"
+  resolved "https://registry.yarnpkg.com/bfj-node4/-/bfj-node4-5.3.1.tgz#e23d8b27057f1d0214fc561142ad9db998f26830"
+  dependencies:
+    bluebird "^3.5.1"
+    check-types "^7.3.0"
+    tryer "^1.0.0"
+
+big.js@^3.1.3:
+  version "3.2.0"
+  resolved "https://registry.yarnpkg.com/big.js/-/big.js-3.2.0.tgz#a5fc298b81b9e0dca2e458824784b65c52ba588e"
+
+binary-extensions@^1.0.0:
+  version "1.11.0"
+  resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-1.11.0.tgz#46aa1751fb6a2f93ee5e689bb1087d4b14c6c205"
+
+blob@0.0.4:
+  version "0.0.4"
+  resolved "https://registry.yarnpkg.com/blob/-/blob-0.0.4.tgz#bcf13052ca54463f30f9fc7e95b9a47630a94921"
+
+block-stream@*:
+  version "0.0.9"
+  resolved "https://registry.yarnpkg.com/block-stream/-/block-stream-0.0.9.tgz#13ebfe778a03205cfe03751481ebb4b3300c126a"
+  dependencies:
+    inherits "~2.0.0"
+
+bluebird@^3.0.5, bluebird@^3.1.1, bluebird@^3.3.0, bluebird@^3.4.7, bluebird@^3.5.1:
+  version "3.5.1"
+  resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.5.1.tgz#d9551f9de98f1fcda1e683d17ee91a0602ee2eb9"
+
+bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.1.1, bn.js@^4.4.0:
+  version "4.11.8"
+  resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.11.8.tgz#2cde09eb5ee341f484746bb0309b3253b1b1442f"
+
+body-parser@1.18.2, body-parser@^1.16.1:
+  version "1.18.2"
+  resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.18.2.tgz#87678a19d84b47d859b83199bd59bce222b10454"
+  dependencies:
+    bytes "3.0.0"
+    content-type "~1.0.4"
+    debug "2.6.9"
+    depd "~1.1.1"
+    http-errors "~1.6.2"
+    iconv-lite "0.4.19"
+    on-finished "~2.3.0"
+    qs "6.5.1"
+    raw-body "2.3.2"
+    type-is "~1.6.15"
+
+boolbase@~1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e"
+
+boom@2.x.x:
+  version "2.10.1"
+  resolved "https://registry.yarnpkg.com/boom/-/boom-2.10.1.tgz#39c8918ceff5799f83f9492a848f625add0c766f"
+  dependencies:
+    hoek "2.x.x"
+
+boom@4.x.x:
+  version "4.3.1"
+  resolved "https://registry.yarnpkg.com/boom/-/boom-4.3.1.tgz#4f8a3005cb4a7e3889f749030fd25b96e01d2e31"
+  dependencies:
+    hoek "4.x.x"
+
+boom@5.x.x:
+  version "5.2.0"
+  resolved "https://registry.yarnpkg.com/boom/-/boom-5.2.0.tgz#5dd9da6ee3a5f302077436290cb717d3f4a54e02"
+  dependencies:
+    hoek "4.x.x"
+
+brace-expansion@^1.0.0, brace-expansion@^1.1.7:
+  version "1.1.11"
+  resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd"
+  dependencies:
+    balanced-match "^1.0.0"
+    concat-map "0.0.1"
+
+braces@^0.1.2:
+  version "0.1.5"
+  resolved "https://registry.yarnpkg.com/braces/-/braces-0.1.5.tgz#c085711085291d8b75fdd74eab0f8597280711e6"
+  dependencies:
+    expand-range "^0.1.0"
+
+braces@^1.8.2:
+  version "1.8.5"
+  resolved "https://registry.yarnpkg.com/braces/-/braces-1.8.5.tgz#ba77962e12dff969d6b76711e914b737857bf6a7"
+  dependencies:
+    expand-range "^1.8.1"
+    preserve "^0.2.0"
+    repeat-element "^1.1.2"
+
+braces@^2.3.0, braces@^2.3.1:
+  version "2.3.2"
+  resolved "https://registry.yarnpkg.com/braces/-/braces-2.3.2.tgz#5979fd3f14cd531565e5fa2df1abfff1dfaee729"
+  dependencies:
+    arr-flatten "^1.1.0"
+    array-unique "^0.3.2"
+    extend-shallow "^2.0.1"
+    fill-range "^4.0.0"
+    isobject "^3.0.1"
+    repeat-element "^1.1.2"
+    snapdragon "^0.8.1"
+    snapdragon-node "^2.0.1"
+    split-string "^3.0.2"
+    to-regex "^3.0.1"
+
+brorand@^1.0.1:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/brorand/-/brorand-1.1.0.tgz#12c25efe40a45e3c323eb8675a0a0ce57b22371f"
+
+browser-stdout@1.3.0:
+  version "1.3.0"
+  resolved "https://registry.yarnpkg.com/browser-stdout/-/browser-stdout-1.3.0.tgz#f351d32969d32fa5d7a5567154263d928ae3bd1f"
+
+browserify-aes@^1.0.0, browserify-aes@^1.0.4:
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/browserify-aes/-/browserify-aes-1.2.0.tgz#326734642f403dabc3003209853bb70ad428ef48"
+  dependencies:
+    buffer-xor "^1.0.3"
+    cipher-base "^1.0.0"
+    create-hash "^1.1.0"
+    evp_bytestokey "^1.0.3"
+    inherits "^2.0.1"
+    safe-buffer "^5.0.1"
+
+browserify-cipher@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/browserify-cipher/-/browserify-cipher-1.0.0.tgz#9988244874bf5ed4e28da95666dcd66ac8fc363a"
+  dependencies:
+    browserify-aes "^1.0.4"
+    browserify-des "^1.0.0"
+    evp_bytestokey "^1.0.0"
+
+browserify-des@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/browserify-des/-/browserify-des-1.0.0.tgz#daa277717470922ed2fe18594118a175439721dd"
+  dependencies:
+    cipher-base "^1.0.1"
+    des.js "^1.0.0"
+    inherits "^2.0.1"
+
+browserify-rsa@^4.0.0:
+  version "4.0.1"
+  resolved "https://registry.yarnpkg.com/browserify-rsa/-/browserify-rsa-4.0.1.tgz#21e0abfaf6f2029cf2fafb133567a701d4135524"
+  dependencies:
+    bn.js "^4.1.0"
+    randombytes "^2.0.1"
+
+browserify-sign@^4.0.0:
+  version "4.0.4"
+  resolved "https://registry.yarnpkg.com/browserify-sign/-/browserify-sign-4.0.4.tgz#aa4eb68e5d7b658baa6bf6a57e630cbd7a93d298"
+  dependencies:
+    bn.js "^4.1.1"
+    browserify-rsa "^4.0.0"
+    create-hash "^1.1.0"
+    create-hmac "^1.1.2"
+    elliptic "^6.0.0"
+    inherits "^2.0.1"
+    parse-asn1 "^5.0.0"
+
+browserify-zlib@^0.2.0:
+  version "0.2.0"
+  resolved "https://registry.yarnpkg.com/browserify-zlib/-/browserify-zlib-0.2.0.tgz#2869459d9aa3be245fe8fe2ca1f46e2e7f54d73f"
+  dependencies:
+    pako "~1.0.5"
+
+browserslist@^1.3.6, browserslist@^1.5.2, browserslist@^1.7.6:
+  version "1.7.7"
+  resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-1.7.7.tgz#0bd76704258be829b2398bb50e4b62d1a166b0b9"
+  dependencies:
+    caniuse-db "^1.0.30000639"
+    electron-to-chromium "^1.2.7"
+
+browserslist@^2.1.2:
+  version "2.11.3"
+  resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-2.11.3.tgz#fe36167aed1bbcde4827ebfe71347a2cc70b99b2"
+  dependencies:
+    caniuse-lite "^1.0.30000792"
+    electron-to-chromium "^1.3.30"
+
+buffer-from@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.0.0.tgz#4cb8832d23612589b0406e9e2956c17f06fdf531"
+
+buffer-xor@^1.0.3:
+  version "1.0.3"
+  resolved "https://registry.yarnpkg.com/buffer-xor/-/buffer-xor-1.0.3.tgz#26e61ed1422fb70dd42e6e36729ed51d855fe8d9"
+
+buffer@^4.3.0:
+  version "4.9.1"
+  resolved "https://registry.yarnpkg.com/buffer/-/buffer-4.9.1.tgz#6d1bb601b07a4efced97094132093027c95bc298"
+  dependencies:
+    base64-js "^1.0.2"
+    ieee754 "^1.1.4"
+    isarray "^1.0.0"
+
+builtin-modules@^1.0.0:
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-1.1.1.tgz#270f076c5a72c02f5b65a47df94c5fe3a278892f"
+
+builtin-status-codes@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz#85982878e21b98e1c66425e03d0174788f569ee8"
+
+bytes@3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.0.0.tgz#d32815404d689699f85a4ea4fa8755dd13a96048"
+
+cacache@^10.0.4:
+  version "10.0.4"
+  resolved "https://registry.yarnpkg.com/cacache/-/cacache-10.0.4.tgz#6452367999eff9d4188aefd9a14e9d7c6a263460"
+  dependencies:
+    bluebird "^3.5.1"
+    chownr "^1.0.1"
+    glob "^7.1.2"
+    graceful-fs "^4.1.11"
+    lru-cache "^4.1.1"
+    mississippi "^2.0.0"
+    mkdirp "^0.5.1"
+    move-concurrently "^1.0.1"
+    promise-inflight "^1.0.1"
+    rimraf "^2.6.2"
+    ssri "^5.2.4"
+    unique-filename "^1.1.0"
+    y18n "^4.0.0"
+
+cache-base@^1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/cache-base/-/cache-base-1.0.1.tgz#0a7f46416831c8b662ee36fe4e7c59d76f666ab2"
+  dependencies:
+    collection-visit "^1.0.0"
+    component-emitter "^1.2.1"
+    get-value "^2.0.6"
+    has-value "^1.0.0"
+    isobject "^3.0.1"
+    set-value "^2.0.0"
+    to-object-path "^0.3.0"
+    union-value "^1.0.0"
+    unset-value "^1.0.0"
+
+caller-path@^0.1.0:
+  version "0.1.0"
+  resolved "https://registry.yarnpkg.com/caller-path/-/caller-path-0.1.0.tgz#94085ef63581ecd3daa92444a8fe94e82577751f"
+  dependencies:
+    callsites "^0.2.0"
+
+callsite@1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/callsite/-/callsite-1.0.0.tgz#280398e5d664bd74038b6f0905153e6e8af1bc20"
+
+callsites@^0.2.0:
+  version "0.2.0"
+  resolved "https://registry.yarnpkg.com/callsites/-/callsites-0.2.0.tgz#afab96262910a7f33c19a5775825c69f34e350ca"
+
+camel-case@3.0.x:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/camel-case/-/camel-case-3.0.0.tgz#ca3c3688a4e9cf3a4cda777dc4dcbc713249cf73"
+  dependencies:
+    no-case "^2.2.0"
+    upper-case "^1.1.1"
+
+camelcase-keys@^2.0.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/camelcase-keys/-/camelcase-keys-2.1.0.tgz#308beeaffdf28119051efa1d932213c91b8f92e7"
+  dependencies:
+    camelcase "^2.0.0"
+    map-obj "^1.0.0"
+
+camelcase@^1.0.2:
+  version "1.2.1"
+  resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-1.2.1.tgz#9bb5304d2e0b56698b2c758b08a3eaa9daa58a39"
+
+camelcase@^2.0.0:
+  version "2.1.1"
+  resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-2.1.1.tgz#7c1d16d679a1bbe59ca02cacecfb011e201f5a1f"
+
+camelcase@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-3.0.0.tgz#32fc4b9fcdaf845fcdf7e73bb97cac2261f0ab0a"
+
+camelcase@^4.1.0:
+  version "4.1.0"
+  resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-4.1.0.tgz#d545635be1e33c542649c69173e5de6acfae34dd"
+
+caniuse-api@^1.5.2:
+  version "1.6.1"
+  resolved "https://registry.yarnpkg.com/caniuse-api/-/caniuse-api-1.6.1.tgz#b534e7c734c4f81ec5fbe8aca2ad24354b962c6c"
+  dependencies:
+    browserslist "^1.3.6"
+    caniuse-db "^1.0.30000529"
+    lodash.memoize "^4.1.2"
+    lodash.uniq "^4.5.0"
+
+caniuse-db@^1.0.30000529, caniuse-db@^1.0.30000634, caniuse-db@^1.0.30000639:
+  version "1.0.30000824"
+  resolved "https://registry.yarnpkg.com/caniuse-db/-/caniuse-db-1.0.30000824.tgz#bba3ff425296e04caa37fe426259206a7056551b"
+
+caniuse-lite@^1.0.30000792:
+  version "1.0.30000824"
+  resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30000824.tgz#de3bc1ba0bff4937302f8cb2a8632a8cc1c07f9a"
+
+caseless@~0.11.0:
+  version "0.11.0"
+  resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.11.0.tgz#715b96ea9841593cc33067923f5ec60ebda4f7d7"
+
+caseless@~0.12.0:
+  version "0.12.0"
+  resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc"
+
+center-align@^0.1.1:
+  version "0.1.3"
+  resolved "https://registry.yarnpkg.com/center-align/-/center-align-0.1.3.tgz#aa0d32629b6ee972200411cbd4461c907bc2b7ad"
+  dependencies:
+    align-text "^0.1.3"
+    lazy-cache "^1.0.3"
+
+chai-nightwatch@~0.1.x:
+  version "0.1.1"
+  resolved "https://registry.yarnpkg.com/chai-nightwatch/-/chai-nightwatch-0.1.1.tgz#1ca56de768d3c0868fe7fc2f4d32c2fe894e6be9"
+  dependencies:
+    assertion-error "1.0.0"
+    deep-eql "0.1.3"
+
+chai@^3.5.0:
+  version "3.5.0"
+  resolved "https://registry.yarnpkg.com/chai/-/chai-3.5.0.tgz#4d02637b067fe958bdbfdd3a40ec56fef7373247"
+  dependencies:
+    assertion-error "^1.0.1"
+    deep-eql "^0.1.3"
+    type-detect "^1.0.0"
+
+chalk@^1.0.0, chalk@^1.1.1, chalk@^1.1.3:
+  version "1.1.3"
+  resolved "https://registry.yarnpkg.com/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98"
+  dependencies:
+    ansi-styles "^2.2.1"
+    escape-string-regexp "^1.0.2"
+    has-ansi "^2.0.0"
+    strip-ansi "^3.0.0"
+    supports-color "^2.0.0"
+
+chalk@^2.0.1, chalk@^2.1.0, chalk@^2.3.0, chalk@^2.3.2:
+  version "2.3.2"
+  resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.3.2.tgz#250dc96b07491bfd601e648d66ddf5f60c7a5c65"
+  dependencies:
+    ansi-styles "^3.2.1"
+    escape-string-regexp "^1.0.5"
+    supports-color "^5.3.0"
+
+check-types@^7.3.0:
+  version "7.3.0"
+  resolved "https://registry.yarnpkg.com/check-types/-/check-types-7.3.0.tgz#468f571a4435c24248f5fd0cb0e8d87c3c341e7d"
+
+cheerio@^0.22.0:
+  version "0.22.0"
+  resolved "https://registry.yarnpkg.com/cheerio/-/cheerio-0.22.0.tgz#a9baa860a3f9b595a6b81b1a86873121ed3a269e"
+  dependencies:
+    css-select "~1.2.0"
+    dom-serializer "~0.1.0"
+    entities "~1.1.1"
+    htmlparser2 "^3.9.1"
+    lodash.assignin "^4.0.9"
+    lodash.bind "^4.1.4"
+    lodash.defaults "^4.0.1"
+    lodash.filter "^4.4.0"
+    lodash.flatten "^4.2.0"
+    lodash.foreach "^4.3.0"
+    lodash.map "^4.4.0"
+    lodash.merge "^4.4.0"
+    lodash.pick "^4.2.1"
+    lodash.reduce "^4.4.0"
+    lodash.reject "^4.4.0"
+    lodash.some "^4.4.0"
+
+chokidar@^1.4.1:
+  version "1.7.0"
+  resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-1.7.0.tgz#798e689778151c8076b4b360e5edd28cda2bb468"
+  dependencies:
+    anymatch "^1.3.0"
+    async-each "^1.0.0"
+    glob-parent "^2.0.0"
+    inherits "^2.0.1"
+    is-binary-path "^1.0.0"
+    is-glob "^2.0.0"
+    path-is-absolute "^1.0.0"
+    readdirp "^2.0.0"
+  optionalDependencies:
+    fsevents "^1.0.0"
+
+chokidar@^2.0.2:
+  version "2.0.3"
+  resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-2.0.3.tgz#dcbd4f6cbb2a55b4799ba8a840ac527e5f4b1176"
+  dependencies:
+    anymatch "^2.0.0"
+    async-each "^1.0.0"
+    braces "^2.3.0"
+    glob-parent "^3.1.0"
+    inherits "^2.0.1"
+    is-binary-path "^1.0.0"
+    is-glob "^4.0.0"
+    normalize-path "^2.1.1"
+    path-is-absolute "^1.0.0"
+    readdirp "^2.0.0"
+    upath "^1.0.0"
+  optionalDependencies:
+    fsevents "^1.1.2"
+
+chownr@^1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.0.1.tgz#e2a75042a9551908bebd25b8523d5f9769d79181"
+
+chromedriver@^2.27.2:
+  version "2.37.0"
+  resolved "https://registry.yarnpkg.com/chromedriver/-/chromedriver-2.37.0.tgz#e7867c8236f6bb89024737bbffc9a4b33ded658b"
+  dependencies:
+    del "^3.0.0"
+    extract-zip "^1.6.5"
+    kew "^0.7.0"
+    mkdirp "^0.5.1"
+    request "^2.83.0"
+
+cipher-base@^1.0.0, cipher-base@^1.0.1, cipher-base@^1.0.3:
+  version "1.0.4"
+  resolved "https://registry.yarnpkg.com/cipher-base/-/cipher-base-1.0.4.tgz#8760e4ecc272f4c363532f926d874aae2c1397de"
+  dependencies:
+    inherits "^2.0.1"
+    safe-buffer "^5.0.1"
+
+circular-json@^0.3.1:
+  version "0.3.3"
+  resolved "https://registry.yarnpkg.com/circular-json/-/circular-json-0.3.3.tgz#815c99ea84f6809529d2f45791bdf82711352d66"
+
+clap@^1.0.9:
+  version "1.2.3"
+  resolved "https://registry.yarnpkg.com/clap/-/clap-1.2.3.tgz#4f36745b32008492557f46412d66d50cb99bce51"
+  dependencies:
+    chalk "^1.1.3"
+
+class-utils@^0.3.5:
+  version "0.3.6"
+  resolved "https://registry.yarnpkg.com/class-utils/-/class-utils-0.3.6.tgz#f93369ae8b9a7ce02fd41faad0ca83033190c463"
+  dependencies:
+    arr-union "^3.1.0"
+    define-property "^0.2.5"
+    isobject "^3.0.0"
+    static-extend "^0.1.1"
+
+clean-css@4.1.x:
+  version "4.1.11"
+  resolved "https://registry.yarnpkg.com/clean-css/-/clean-css-4.1.11.tgz#2ecdf145aba38f54740f26cefd0ff3e03e125d6a"
+  dependencies:
+    source-map "0.5.x"
+
+cli-cursor@^1.0.1:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-1.0.2.tgz#64da3f7d56a54412e59794bd62dc35295e8f2987"
+  dependencies:
+    restore-cursor "^1.0.1"
+
+cli-cursor@^2.1.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-2.1.0.tgz#b35dac376479facc3e94747d41d0d0f5238ffcb5"
+  dependencies:
+    restore-cursor "^2.0.0"
+
+cli-spinners@^1.0.1:
+  version "1.3.1"
+  resolved "https://registry.yarnpkg.com/cli-spinners/-/cli-spinners-1.3.1.tgz#002c1990912d0d59580c93bd36c056de99e4259a"
+
+cli-width@^2.0.0:
+  version "2.2.0"
+  resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-2.2.0.tgz#ff19ede8a9a5e579324147b0c11f0fbcbabed639"
+
+cliui@^2.1.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/cliui/-/cliui-2.1.0.tgz#4b475760ff80264c762c3a1719032e91c7fea0d1"
+  dependencies:
+    center-align "^0.1.1"
+    right-align "^0.1.1"
+    wordwrap "0.0.2"
+
+cliui@^3.2.0:
+  version "3.2.0"
+  resolved "https://registry.yarnpkg.com/cliui/-/cliui-3.2.0.tgz#120601537a916d29940f934da3b48d585a39213d"
+  dependencies:
+    string-width "^1.0.1"
+    strip-ansi "^3.0.1"
+    wrap-ansi "^2.0.0"
+
+cliui@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/cliui/-/cliui-4.0.0.tgz#743d4650e05f36d1ed2575b59638d87322bfbbcc"
+  dependencies:
+    string-width "^2.1.1"
+    strip-ansi "^4.0.0"
+    wrap-ansi "^2.0.0"
+
+clone-deep@^2.0.1:
+  version "2.0.2"
+  resolved "https://registry.yarnpkg.com/clone-deep/-/clone-deep-2.0.2.tgz#00db3a1e173656730d1188c3d6aced6d7ea97713"
+  dependencies:
+    for-own "^1.0.0"
+    is-plain-object "^2.0.4"
+    kind-of "^6.0.0"
+    shallow-clone "^1.0.0"
+
+clone@^1.0.2:
+  version "1.0.4"
+  resolved "https://registry.yarnpkg.com/clone/-/clone-1.0.4.tgz#da309cc263df15994c688ca902179ca3c7cd7c7e"
+
+co@^4.6.0:
+  version "4.6.0"
+  resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184"
+
+co@~3.0.6:
+  version "3.0.6"
+  resolved "https://registry.yarnpkg.com/co/-/co-3.0.6.tgz#1445f226c5eb956138e68c9ac30167ea7d2e6bda"
+
+coa@~1.0.1:
+  version "1.0.4"
+  resolved "https://registry.yarnpkg.com/coa/-/coa-1.0.4.tgz#a9ef153660d6a86a8bdec0289a5c684d217432fd"
+  dependencies:
+    q "^1.1.2"
+
+code-point-at@^1.0.0:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77"
+
+collection-visit@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/collection-visit/-/collection-visit-1.0.0.tgz#4bc0373c164bc3291b4d368c829cf1a80a59dca0"
+  dependencies:
+    map-visit "^1.0.0"
+    object-visit "^1.0.0"
+
+color-convert@^1.3.0, color-convert@^1.9.0:
+  version "1.9.1"
+  resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.1.tgz#c1261107aeb2f294ebffec9ed9ecad529a6097ed"
+  dependencies:
+    color-name "^1.1.1"
+
+color-name@^1.0.0, color-name@^1.1.1:
+  version "1.1.3"
+  resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25"
+
+color-string@^0.3.0:
+  version "0.3.0"
+  resolved "https://registry.yarnpkg.com/color-string/-/color-string-0.3.0.tgz#27d46fb67025c5c2fa25993bfbf579e47841b991"
+  dependencies:
+    color-name "^1.0.0"
+
+color@^0.11.0:
+  version "0.11.4"
+  resolved "https://registry.yarnpkg.com/color/-/color-0.11.4.tgz#6d7b5c74fb65e841cd48792ad1ed5e07b904d764"
+  dependencies:
+    clone "^1.0.2"
+    color-convert "^1.3.0"
+    color-string "^0.3.0"
+
+colormin@^1.0.5:
+  version "1.1.2"
+  resolved "https://registry.yarnpkg.com/colormin/-/colormin-1.1.2.tgz#ea2f7420a72b96881a38aae59ec124a6f7298133"
+  dependencies:
+    color "^0.11.0"
+    css-color-names "0.0.4"
+    has "^1.0.1"
+
+colors@^1.1.0, colors@^1.1.2:
+  version "1.2.1"
+  resolved "https://registry.yarnpkg.com/colors/-/colors-1.2.1.tgz#f4a3d302976aaf042356ba1ade3b1a2c62d9d794"
+
+colors@~1.1.2:
+  version "1.1.2"
+  resolved "https://registry.yarnpkg.com/colors/-/colors-1.1.2.tgz#168a4701756b6a7f51a12ce0c97bfa28c084ed63"
+
+combine-lists@^1.0.0:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/combine-lists/-/combine-lists-1.0.1.tgz#458c07e09e0d900fc28b70a3fec2dacd1d2cb7f6"
+  dependencies:
+    lodash "^4.5.0"
+
+combined-stream@1.0.6, combined-stream@^1.0.5, combined-stream@~1.0.5:
+  version "1.0.6"
+  resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.6.tgz#723e7df6e801ac5613113a7e445a9b69cb632818"
+  dependencies:
+    delayed-stream "~1.0.0"
+
+commander@2.15.x, commander@^2.11.0, commander@^2.13.0, commander@^2.9.0, commander@~2.15.0:
+  version "2.15.1"
+  resolved "https://registry.yarnpkg.com/commander/-/commander-2.15.1.tgz#df46e867d0fc2aec66a34662b406a9ccafff5b0f"
+
+commander@2.9.0:
+  version "2.9.0"
+  resolved "https://registry.yarnpkg.com/commander/-/commander-2.9.0.tgz#9c99094176e12240cb22d6c5146098400fe0f7d4"
+  dependencies:
+    graceful-readlink ">= 1.0.0"
+
+commondir@^1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b"
+
+component-bind@1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/component-bind/-/component-bind-1.0.0.tgz#00c608ab7dcd93897c0009651b1d3a8e1e73bbd1"
+
+component-emitter@1.1.2:
+  version "1.1.2"
+  resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.1.2.tgz#296594f2753daa63996d2af08d15a95116c9aec3"
+
+component-emitter@1.2.1, component-emitter@^1.2.1:
+  version "1.2.1"
+  resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.2.1.tgz#137918d6d78283f7df7a6b7c5a63e140e69425e6"
+
+component-inherit@0.0.3:
+  version "0.0.3"
+  resolved "https://registry.yarnpkg.com/component-inherit/-/component-inherit-0.0.3.tgz#645fc4adf58b72b649d5cae65135619db26ff143"
+
+concat-map@0.0.1:
+  version "0.0.1"
+  resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
+
+concat-stream@1.6.0:
+  version "1.6.0"
+  resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.6.0.tgz#0aac662fd52be78964d5532f694784e70110acf7"
+  dependencies:
+    inherits "^2.0.3"
+    readable-stream "^2.2.2"
+    typedarray "^0.0.6"
+
+concat-stream@^1.5.0, concat-stream@^1.5.2:
+  version "1.6.2"
+  resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.6.2.tgz#904bdf194cd3122fc675c77fc4ac3d4ff0fd1a34"
+  dependencies:
+    buffer-from "^1.0.0"
+    inherits "^2.0.3"
+    readable-stream "^2.2.2"
+    typedarray "^0.0.6"
+
+config-chain@~1.1.5:
+  version "1.1.11"
+  resolved "https://registry.yarnpkg.com/config-chain/-/config-chain-1.1.11.tgz#aba09747dfbe4c3e70e766a6e41586e1859fc6f2"
+  dependencies:
+    ini "^1.3.4"
+    proto-list "~1.2.1"
+
+connect-history-api-fallback@^1.3.0:
+  version "1.5.0"
+  resolved "https://registry.yarnpkg.com/connect-history-api-fallback/-/connect-history-api-fallback-1.5.0.tgz#b06873934bc5e344fef611a196a6faae0aee015a"
+
+connect@^3.6.0:
+  version "3.6.6"
+  resolved "https://registry.yarnpkg.com/connect/-/connect-3.6.6.tgz#09eff6c55af7236e137135a72574858b6786f524"
+  dependencies:
+    debug "2.6.9"
+    finalhandler "1.1.0"
+    parseurl "~1.3.2"
+    utils-merge "1.0.1"
+
+console-browserify@^1.1.0:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/console-browserify/-/console-browserify-1.1.0.tgz#f0241c45730a9fc6323b206dbf38edc741d0bb10"
+  dependencies:
+    date-now "^0.1.4"
+
+console-control-strings@^1.0.0, console-control-strings@~1.1.0:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/console-control-strings/-/console-control-strings-1.1.0.tgz#3d7cf4464db6446ea644bf4b39507f9851008e8e"
+
+consolidate@^0.14.0:
+  version "0.14.5"
+  resolved "https://registry.yarnpkg.com/consolidate/-/consolidate-0.14.5.tgz#5a25047bc76f73072667c8cb52c989888f494c63"
+  dependencies:
+    bluebird "^3.1.1"
+
+constants-browserify@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/constants-browserify/-/constants-browserify-1.0.0.tgz#c20b96d8c617748aaf1c16021760cd27fcb8cb75"
+
+content-disposition@0.5.2:
+  version "0.5.2"
+  resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.2.tgz#0cf68bb9ddf5f2be7961c3a85178cb85dba78cb4"
+
+content-type@~1.0.4:
+  version "1.0.4"
+  resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b"
+
+convert-source-map@^1.5.0:
+  version "1.5.1"
+  resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.5.1.tgz#b8278097b9bc229365de5c62cf5fcaed8b5599e5"
+
+cookie-signature@1.0.6:
+  version "1.0.6"
+  resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c"
+
+cookie@0.3.1:
+  version "0.3.1"
+  resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.3.1.tgz#e7e0a1f9ef43b4c8ba925c5c5a96e806d16873bb"
+
+copy-concurrently@^1.0.0:
+  version "1.0.5"
+  resolved "https://registry.yarnpkg.com/copy-concurrently/-/copy-concurrently-1.0.5.tgz#92297398cae34937fcafd6ec8139c18051f0b5e0"
+  dependencies:
+    aproba "^1.1.1"
+    fs-write-stream-atomic "^1.0.8"
+    iferr "^0.1.5"
+    mkdirp "^0.5.1"
+    rimraf "^2.5.4"
+    run-queue "^1.0.0"
+
+copy-descriptor@^0.1.0:
+  version "0.1.1"
+  resolved "https://registry.yarnpkg.com/copy-descriptor/-/copy-descriptor-0.1.1.tgz#676f6eb3c39997c2ee1ac3a924fd6124748f578d"
+
+copy-webpack-plugin@^4.0.1:
+  version "4.5.1"
+  resolved "https://registry.yarnpkg.com/copy-webpack-plugin/-/copy-webpack-plugin-4.5.1.tgz#fc4f68f4add837cc5e13d111b20715793225d29c"
+  dependencies:
+    cacache "^10.0.4"
+    find-cache-dir "^1.0.0"
+    globby "^7.1.1"
+    is-glob "^4.0.0"
+    loader-utils "^1.1.0"
+    minimatch "^3.0.4"
+    p-limit "^1.0.0"
+    serialize-javascript "^1.4.0"
+
+core-js@^2.2.0, core-js@^2.4.0, core-js@^2.5.0:
+  version "2.5.4"
+  resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.5.4.tgz#f2c8bf181f2a80b92f360121429ce63a2f0aeae0"
+
+core-util-is@1.0.2, core-util-is@~1.0.0:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7"
+
+cosmiconfig@^2.1.0, cosmiconfig@^2.1.1:
+  version "2.2.2"
+  resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-2.2.2.tgz#6173cebd56fac042c1f4390edf7af6c07c7cb892"
+  dependencies:
+    is-directory "^0.3.1"
+    js-yaml "^3.4.3"
+    minimist "^1.2.0"
+    object-assign "^4.1.0"
+    os-homedir "^1.0.1"
+    parse-json "^2.2.0"
+    require-from-string "^1.1.0"
+
+create-ecdh@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/create-ecdh/-/create-ecdh-4.0.0.tgz#888c723596cdf7612f6498233eebd7a35301737d"
+  dependencies:
+    bn.js "^4.1.0"
+    elliptic "^6.0.0"
+
+create-hash@^1.1.0, create-hash@^1.1.2:
+  version "1.1.3"
+  resolved "https://registry.yarnpkg.com/create-hash/-/create-hash-1.1.3.tgz#606042ac8b9262750f483caddab0f5819172d8fd"
+  dependencies:
+    cipher-base "^1.0.1"
+    inherits "^2.0.1"
+    ripemd160 "^2.0.0"
+    sha.js "^2.4.0"
+
+create-hmac@^1.1.0, create-hmac@^1.1.2, create-hmac@^1.1.4:
+  version "1.1.6"
+  resolved "https://registry.yarnpkg.com/create-hmac/-/create-hmac-1.1.6.tgz#acb9e221a4e17bdb076e90657c42b93e3726cf06"
+  dependencies:
+    cipher-base "^1.0.3"
+    create-hash "^1.1.0"
+    inherits "^2.0.1"
+    ripemd160 "^2.0.0"
+    safe-buffer "^5.0.1"
+    sha.js "^2.4.8"
+
+cross-env@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/cross-env/-/cross-env-4.0.0.tgz#16083862d08275a4628b0b243b121bedaa55dd80"
+  dependencies:
+    cross-spawn "^5.1.0"
+    is-windows "^1.0.0"
+
+cross-spawn@^3.0.0:
+  version "3.0.1"
+  resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-3.0.1.tgz#1256037ecb9f0c5f79e3d6ef135e30770184b982"
+  dependencies:
+    lru-cache "^4.0.1"
+    which "^1.2.9"
+
+cross-spawn@^5.0.1, cross-spawn@^5.1.0:
+  version "5.1.0"
+  resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-5.1.0.tgz#e8bd0efee58fcff6f8f94510a0a554bbfa235449"
+  dependencies:
+    lru-cache "^4.0.1"
+    shebang-command "^1.2.0"
+    which "^1.2.9"
+
+cryptiles@2.x.x:
+  version "2.0.5"
+  resolved "https://registry.yarnpkg.com/cryptiles/-/cryptiles-2.0.5.tgz#3bdfecdc608147c1c67202fa291e7dca59eaa3b8"
+  dependencies:
+    boom "2.x.x"
+
+cryptiles@3.x.x:
+  version "3.1.2"
+  resolved "https://registry.yarnpkg.com/cryptiles/-/cryptiles-3.1.2.tgz#a89fbb220f5ce25ec56e8c4aa8a4fd7b5b0d29fe"
+  dependencies:
+    boom "5.x.x"
+
+crypto-browserify@^3.11.0:
+  version "3.12.0"
+  resolved "https://registry.yarnpkg.com/crypto-browserify/-/crypto-browserify-3.12.0.tgz#396cf9f3137f03e4b8e532c58f698254e00f80ec"
+  dependencies:
+    browserify-cipher "^1.0.0"
+    browserify-sign "^4.0.0"
+    create-ecdh "^4.0.0"
+    create-hash "^1.1.0"
+    create-hmac "^1.1.0"
+    diffie-hellman "^5.0.0"
+    inherits "^2.0.1"
+    pbkdf2 "^3.0.3"
+    public-encrypt "^4.0.0"
+    randombytes "^2.0.0"
+    randomfill "^1.0.3"
+
+css-color-names@0.0.4:
+  version "0.0.4"
+  resolved "https://registry.yarnpkg.com/css-color-names/-/css-color-names-0.0.4.tgz#808adc2e79cf84738069b646cb20ec27beb629e0"
+
+css-loader@^0.28.0:
+  version "0.28.11"
+  resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-0.28.11.tgz#c3f9864a700be2711bb5a2462b2389b1a392dab7"
+  dependencies:
+    babel-code-frame "^6.26.0"
+    css-selector-tokenizer "^0.7.0"
+    cssnano "^3.10.0"
+    icss-utils "^2.1.0"
+    loader-utils "^1.0.2"
+    lodash.camelcase "^4.3.0"
+    object-assign "^4.1.1"
+    postcss "^5.0.6"
+    postcss-modules-extract-imports "^1.2.0"
+    postcss-modules-local-by-default "^1.2.0"
+    postcss-modules-scope "^1.1.0"
+    postcss-modules-values "^1.3.0"
+    postcss-value-parser "^3.3.0"
+    source-list-map "^2.0.0"
+
+css-select@^1.1.0, css-select@~1.2.0:
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/css-select/-/css-select-1.2.0.tgz#2b3a110539c5355f1cd8d314623e870b121ec858"
+  dependencies:
+    boolbase "~1.0.0"
+    css-what "2.1"
+    domutils "1.5.1"
+    nth-check "~1.0.1"
+
+css-selector-tokenizer@^0.7.0:
+  version "0.7.0"
+  resolved "https://registry.yarnpkg.com/css-selector-tokenizer/-/css-selector-tokenizer-0.7.0.tgz#e6988474ae8c953477bf5e7efecfceccd9cf4c86"
+  dependencies:
+    cssesc "^0.1.0"
+    fastparse "^1.1.1"
+    regexpu-core "^1.0.0"
+
+css-what@2.1:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/css-what/-/css-what-2.1.0.tgz#9467d032c38cfaefb9f2d79501253062f87fa1bd"
+
+cssesc@^0.1.0:
+  version "0.1.0"
+  resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-0.1.0.tgz#c814903e45623371a0477b40109aaafbeeaddbb4"
+
+cssnano@^3.10.0, cssnano@^3.4.0:
+  version "3.10.0"
+  resolved "https://registry.yarnpkg.com/cssnano/-/cssnano-3.10.0.tgz#4f38f6cea2b9b17fa01490f23f1dc68ea65c1c38"
+  dependencies:
+    autoprefixer "^6.3.1"
+    decamelize "^1.1.2"
+    defined "^1.0.0"
+    has "^1.0.1"
+    object-assign "^4.0.1"
+    postcss "^5.0.14"
+    postcss-calc "^5.2.0"
+    postcss-colormin "^2.1.8"
+    postcss-convert-values "^2.3.4"
+    postcss-discard-comments "^2.0.4"
+    postcss-discard-duplicates "^2.0.1"
+    postcss-discard-empty "^2.0.1"
+    postcss-discard-overridden "^0.1.1"
+    postcss-discard-unused "^2.2.1"
+    postcss-filter-plugins "^2.0.0"
+    postcss-merge-idents "^2.1.5"
+    postcss-merge-longhand "^2.0.1"
+    postcss-merge-rules "^2.0.3"
+    postcss-minify-font-values "^1.0.2"
+    postcss-minify-gradients "^1.0.1"
+    postcss-minify-params "^1.0.4"
+    postcss-minify-selectors "^2.0.4"
+    postcss-normalize-charset "^1.1.0"
+    postcss-normalize-url "^3.0.7"
+    postcss-ordered-values "^2.1.0"
+    postcss-reduce-idents "^2.2.2"
+    postcss-reduce-initial "^1.0.0"
+    postcss-reduce-transforms "^1.0.3"
+    postcss-svgo "^2.1.1"
+    postcss-unique-selectors "^2.0.2"
+    postcss-value-parser "^3.2.3"
+    postcss-zindex "^2.0.1"
+
+csso@~2.3.1:
+  version "2.3.2"
+  resolved "https://registry.yarnpkg.com/csso/-/csso-2.3.2.tgz#ddd52c587033f49e94b71fc55569f252e8ff5f85"
+  dependencies:
+    clap "^1.0.9"
+    source-map "^0.5.3"
+
+currently-unhandled@^0.4.1:
+  version "0.4.1"
+  resolved "https://registry.yarnpkg.com/currently-unhandled/-/currently-unhandled-0.4.1.tgz#988df33feab191ef799a61369dd76c17adf957ea"
+  dependencies:
+    array-find-index "^1.0.1"
+
+custom-event@~1.0.0:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/custom-event/-/custom-event-1.0.1.tgz#5d02a46850adf1b4a317946a3928fccb5bfd0425"
+
+cyclist@~0.2.2:
+  version "0.2.2"
+  resolved "https://registry.yarnpkg.com/cyclist/-/cyclist-0.2.2.tgz#1b33792e11e914a2fd6d6ed6447464444e5fa640"
+
+d@1:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/d/-/d-1.0.0.tgz#754bb5bfe55451da69a58b94d45f4c5b0462d58f"
+  dependencies:
+    es5-ext "^0.10.9"
+
+dashdash@^1.12.0:
+  version "1.14.1"
+  resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0"
+  dependencies:
+    assert-plus "^1.0.0"
+
+data-uri-to-buffer@1:
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/data-uri-to-buffer/-/data-uri-to-buffer-1.2.0.tgz#77163ea9c20d8641b4707e8f18abdf9a78f34835"
+
+date-now@^0.1.4:
+  version "0.1.4"
+  resolved "https://registry.yarnpkg.com/date-now/-/date-now-0.1.4.tgz#eaf439fd4d4848ad74e5cc7dbef200672b9e345b"
+
+dateformat@^1.0.6:
+  version "1.0.12"
+  resolved "https://registry.yarnpkg.com/dateformat/-/dateformat-1.0.12.tgz#9f124b67594c937ff706932e4a642cca8dbbfee9"
+  dependencies:
+    get-stdin "^4.0.1"
+    meow "^3.3.0"
+
+dateformat@^2.0.0:
+  version "2.2.0"
+  resolved "https://registry.yarnpkg.com/dateformat/-/dateformat-2.2.0.tgz#4065e2013cf9fb916ddfd82efb506ad4c6769062"
+
+de-indent@^1.0.2:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/de-indent/-/de-indent-1.0.2.tgz#b2038e846dc33baa5796128d0804b455b8c1e21d"
+
+debug@2, debug@2.6.9, debug@^2.1.1, debug@^2.2.0, debug@^2.3.3, debug@^2.6.8:
+  version "2.6.9"
+  resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f"
+  dependencies:
+    ms "2.0.0"
+
+debug@2.2.0:
+  version "2.2.0"
+  resolved "https://registry.yarnpkg.com/debug/-/debug-2.2.0.tgz#f87057e995b1a1f6ae6a4960664137bc56f039da"
+  dependencies:
+    ms "0.7.1"
+
+debug@2.3.3:
+  version "2.3.3"
+  resolved "https://registry.yarnpkg.com/debug/-/debug-2.3.3.tgz#40c453e67e6e13c901ddec317af8986cda9eff8c"
+  dependencies:
+    ms "0.7.2"
+
+debug@2.6.8:
+  version "2.6.8"
+  resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.8.tgz#e731531ca2ede27d188222427da17821d68ff4fc"
+  dependencies:
+    ms "2.0.0"
+
+debug@^3.1.0:
+  version "3.1.0"
+  resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261"
+  dependencies:
+    ms "2.0.0"
+
+decamelize@^1.0.0, decamelize@^1.1.1, decamelize@^1.1.2:
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290"
+
+decode-uri-component@^0.2.0:
+  version "0.2.0"
+  resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.0.tgz#eb3913333458775cb84cd1a1fae062106bb87545"
+
+deep-eql@0.1.3, deep-eql@^0.1.3:
+  version "0.1.3"
+  resolved "https://registry.yarnpkg.com/deep-eql/-/deep-eql-0.1.3.tgz#ef558acab8de25206cd713906d74e56930eb69f2"
+  dependencies:
+    type-detect "0.1.1"
+
+deep-extend@~0.4.0:
+  version "0.4.2"
+  resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.4.2.tgz#48b699c27e334bf89f10892be432f6e4c7d34a7f"
+
+deep-is@~0.1.3:
+  version "0.1.3"
+  resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.3.tgz#b369d6fb5dbc13eecf524f91b070feedc357cf34"
+
+deepmerge@^2.0.0, deepmerge@^2.1.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-2.1.0.tgz#511a54fff405fc346f0240bb270a3e9533a31102"
+
+define-property@^0.2.5:
+  version "0.2.5"
+  resolved "https://registry.yarnpkg.com/define-property/-/define-property-0.2.5.tgz#c35b1ef918ec3c990f9a5bc57be04aacec5c8116"
+  dependencies:
+    is-descriptor "^0.1.0"
+
+define-property@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/define-property/-/define-property-1.0.0.tgz#769ebaaf3f4a63aad3af9e8d304c9bbe79bfb0e6"
+  dependencies:
+    is-descriptor "^1.0.0"
+
+define-property@^2.0.2:
+  version "2.0.2"
+  resolved "https://registry.yarnpkg.com/define-property/-/define-property-2.0.2.tgz#d459689e8d654ba77e02a817f8710d702cb16e9d"
+  dependencies:
+    is-descriptor "^1.0.2"
+    isobject "^3.0.1"
+
+defined@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/defined/-/defined-1.0.0.tgz#c98d9bcef75674188e110969151199e39b1fa693"
+
+degenerator@~1.0.2:
+  version "1.0.4"
+  resolved "https://registry.yarnpkg.com/degenerator/-/degenerator-1.0.4.tgz#fcf490a37ece266464d9cc431ab98c5819ced095"
+  dependencies:
+    ast-types "0.x.x"
+    escodegen "1.x.x"
+    esprima "3.x.x"
+
+del@^2.0.2:
+  version "2.2.2"
+  resolved "https://registry.yarnpkg.com/del/-/del-2.2.2.tgz#c12c981d067846c84bcaf862cff930d907ffd1a8"
+  dependencies:
+    globby "^5.0.0"
+    is-path-cwd "^1.0.0"
+    is-path-in-cwd "^1.0.0"
+    object-assign "^4.0.1"
+    pify "^2.0.0"
+    pinkie-promise "^2.0.0"
+    rimraf "^2.2.8"
+
+del@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/del/-/del-3.0.0.tgz#53ecf699ffcbcb39637691ab13baf160819766e5"
+  dependencies:
+    globby "^6.1.0"
+    is-path-cwd "^1.0.0"
+    is-path-in-cwd "^1.0.0"
+    p-map "^1.1.1"
+    pify "^3.0.0"
+    rimraf "^2.2.8"
+
+delayed-stream@~1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619"
+
+delegates@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a"
+
+depd@1.1.1:
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.1.tgz#5783b4e1c459f06fa5ca27f991f3d06e7a310359"
+
+depd@~1.1.1, depd@~1.1.2:
+  version "1.1.2"
+  resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9"
+
+des.js@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/des.js/-/des.js-1.0.0.tgz#c074d2e2aa6a8a9a07dbd61f9a15c2cd83ec8ecc"
+  dependencies:
+    inherits "^2.0.1"
+    minimalistic-assert "^1.0.0"
+
+desandro-matches-selector@^2.0.0:
+  version "2.0.2"
+  resolved "https://registry.yarnpkg.com/desandro-matches-selector/-/desandro-matches-selector-2.0.2.tgz#717beed4dc13e7d8f3762f707a6d58a6774218e1"
+
+destroy@~1.0.4:
+  version "1.0.4"
+  resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80"
+
+detect-indent@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/detect-indent/-/detect-indent-4.0.0.tgz#f76d064352cdf43a1cb6ce619c4ee3a9475de208"
+  dependencies:
+    repeating "^2.0.0"
+
+detect-libc@^1.0.2:
+  version "1.0.3"
+  resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-1.0.3.tgz#fa137c4bd698edf55cd5cd02ac559f91a4c4ba9b"
+
+di@^0.0.1:
+  version "0.0.1"
+  resolved "https://registry.yarnpkg.com/di/-/di-0.0.1.tgz#806649326ceaa7caa3306d75d985ea2748ba913c"
+
+diff@1.4.0:
+  version "1.4.0"
+  resolved "https://registry.yarnpkg.com/diff/-/diff-1.4.0.tgz#7f28d2eb9ee7b15a97efd89ce63dcfdaa3ccbabf"
+
+diff@3.2.0:
+  version "3.2.0"
+  resolved "https://registry.yarnpkg.com/diff/-/diff-3.2.0.tgz#c9ce393a4b7cbd0b058a725c93df299027868ff9"
+
+diff@^3.1.0:
+  version "3.5.0"
+  resolved "https://registry.yarnpkg.com/diff/-/diff-3.5.0.tgz#800c0dd1e0a8bfbc95835c202ad220fe317e5a12"
+
+diffie-hellman@^5.0.0:
+  version "5.0.2"
+  resolved "https://registry.yarnpkg.com/diffie-hellman/-/diffie-hellman-5.0.2.tgz#b5835739270cfe26acf632099fded2a07f209e5e"
+  dependencies:
+    bn.js "^4.1.0"
+    miller-rabin "^4.0.0"
+    randombytes "^2.0.0"
+
+dir-glob@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-2.0.0.tgz#0b205d2b6aef98238ca286598a8204d29d0a0034"
+  dependencies:
+    arrify "^1.0.1"
+    path-type "^3.0.0"
+
+django-channels@^1.1.6:
+  version "1.1.6"
+  resolved "https://registry.yarnpkg.com/django-channels/-/django-channels-1.1.6.tgz#51cdf9d818be3d098ef49a49356f2a7a78499180"
+  dependencies:
+    reconnecting-websocket "^3.0.3"
+
+doctrine@^2.0.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-2.1.0.tgz#5cd01fc101621b42c4cd7f5d1a66243716d3f39d"
+  dependencies:
+    esutils "^2.0.2"
+
+dom-converter@~0.1:
+  version "0.1.4"
+  resolved "https://registry.yarnpkg.com/dom-converter/-/dom-converter-0.1.4.tgz#a45ef5727b890c9bffe6d7c876e7b19cb0e17f3b"
+  dependencies:
+    utila "~0.3"
+
+dom-serialize@^2.2.0:
+  version "2.2.1"
+  resolved "https://registry.yarnpkg.com/dom-serialize/-/dom-serialize-2.2.1.tgz#562ae8999f44be5ea3076f5419dcd59eb43ac95b"
+  dependencies:
+    custom-event "~1.0.0"
+    ent "~2.2.0"
+    extend "^3.0.0"
+    void-elements "^2.0.0"
+
+dom-serializer@0, dom-serializer@~0.1.0:
+  version "0.1.0"
+  resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-0.1.0.tgz#073c697546ce0780ce23be4a28e293e40bc30c82"
+  dependencies:
+    domelementtype "~1.1.1"
+    entities "~1.1.1"
+
+domain-browser@^1.1.1:
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/domain-browser/-/domain-browser-1.2.0.tgz#3d31f50191a6749dd1375a7f522e823d42e54eda"
+
+domelementtype@1, domelementtype@^1.3.0:
+  version "1.3.0"
+  resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-1.3.0.tgz#b17aed82e8ab59e52dd9c19b1756e0fc187204c2"
+
+domelementtype@~1.1.1:
+  version "1.1.3"
+  resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-1.1.3.tgz#bd28773e2642881aec51544924299c5cd822185b"
+
+domhandler@2.1:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-2.1.0.tgz#d2646f5e57f6c3bab11cf6cb05d3c0acf7412594"
+  dependencies:
+    domelementtype "1"
+
+domhandler@^2.3.0:
+  version "2.4.1"
+  resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-2.4.1.tgz#892e47000a99be55bbf3774ffea0561d8879c259"
+  dependencies:
+    domelementtype "1"
+
+domutils@1.1:
+  version "1.1.6"
+  resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.1.6.tgz#bddc3de099b9a2efacc51c623f28f416ecc57485"
+  dependencies:
+    domelementtype "1"
+
+domutils@1.5.1:
+  version "1.5.1"
+  resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.5.1.tgz#dcd8488a26f563d61079e48c9f7b7e32373682cf"
+  dependencies:
+    dom-serializer "0"
+    domelementtype "1"
+
+domutils@^1.5.1:
+  version "1.7.0"
+  resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.7.0.tgz#56ea341e834e06e6748af7a1cb25da67ea9f8c2a"
+  dependencies:
+    dom-serializer "0"
+    domelementtype "1"
+
+duplexer@^0.1.1:
+  version "0.1.1"
+  resolved "https://registry.yarnpkg.com/duplexer/-/duplexer-0.1.1.tgz#ace6ff808c1ce66b57d1ebf97977acb02334cfc1"
+
+duplexify@^3.4.2, duplexify@^3.5.3:
+  version "3.5.4"
+  resolved "https://registry.yarnpkg.com/duplexify/-/duplexify-3.5.4.tgz#4bb46c1796eabebeec4ca9a2e66b808cb7a3d8b4"
+  dependencies:
+    end-of-stream "^1.0.0"
+    inherits "^2.0.1"
+    readable-stream "^2.0.0"
+    stream-shift "^1.0.0"
+
+ecc-jsbn@~0.1.1:
+  version "0.1.1"
+  resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.1.tgz#0fc73a9ed5f0d53c38193398523ef7e543777505"
+  dependencies:
+    jsbn "~0.1.0"
+
+editorconfig@^0.13.2:
+  version "0.13.3"
+  resolved "https://registry.yarnpkg.com/editorconfig/-/editorconfig-0.13.3.tgz#e5219e587951d60958fd94ea9a9a008cdeff1b34"
+  dependencies:
+    bluebird "^3.0.5"
+    commander "^2.9.0"
+    lru-cache "^3.2.0"
+    semver "^5.1.0"
+    sigmund "^1.0.1"
+
+ee-first@1.1.1:
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d"
+
+ejs@0.7.1:
+  version "0.7.1"
+  resolved "https://registry.yarnpkg.com/ejs/-/ejs-0.7.1.tgz#6dab41cbdee572986db328a2a8d22040f20fba7c"
+
+ejs@2.5.7:
+  version "2.5.7"
+  resolved "https://registry.yarnpkg.com/ejs/-/ejs-2.5.7.tgz#cc872c168880ae3c7189762fd5ffc00896c9518a"
+
+ejs@^2.5.7:
+  version "2.5.8"
+  resolved "https://registry.yarnpkg.com/ejs/-/ejs-2.5.8.tgz#2ab6954619f225e6193b7ac5f7c39c48fefe4380"
+
+electron-to-chromium@^1.2.7, electron-to-chromium@^1.3.30:
+  version "1.3.42"
+  resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.42.tgz#95c33bf01d0cc405556aec899fe61fd4d76ea0f9"
+
+elliptic@^6.0.0:
+  version "6.4.0"
+  resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.4.0.tgz#cac9af8762c85836187003c8dfe193e5e2eae5df"
+  dependencies:
+    bn.js "^4.4.0"
+    brorand "^1.0.1"
+    hash.js "^1.0.0"
+    hmac-drbg "^1.0.0"
+    inherits "^2.0.1"
+    minimalistic-assert "^1.0.0"
+    minimalistic-crypto-utils "^1.0.0"
+
+emojis-list@^2.0.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-2.1.0.tgz#4daa4d9db00f9819880c79fa457ae5b09a1fd389"
+
+encodeurl@~1.0.1, encodeurl@~1.0.2:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59"
+
+encoding@^0.1.12:
+  version "0.1.12"
+  resolved "https://registry.yarnpkg.com/encoding/-/encoding-0.1.12.tgz#538b66f3ee62cd1ab51ec323829d1f9480c74beb"
+  dependencies:
+    iconv-lite "~0.4.13"
+
+end-of-stream@^1.0.0, end-of-stream@^1.1.0:
+  version "1.4.1"
+  resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.1.tgz#ed29634d19baba463b6ce6b80a37213eab71ec43"
+  dependencies:
+    once "^1.4.0"
+
+engine.io-client@1.8.3:
+  version "1.8.3"
+  resolved "https://registry.yarnpkg.com/engine.io-client/-/engine.io-client-1.8.3.tgz#1798ed93451246453d4c6f635d7a201fe940d5ab"
+  dependencies:
+    component-emitter "1.2.1"
+    component-inherit "0.0.3"
+    debug "2.3.3"
+    engine.io-parser "1.3.2"
+    has-cors "1.1.0"
+    indexof "0.0.1"
+    parsejson "0.0.3"
+    parseqs "0.0.5"
+    parseuri "0.0.5"
+    ws "1.1.2"
+    xmlhttprequest-ssl "1.5.3"
+    yeast "0.1.2"
+
+engine.io-parser@1.3.2:
+  version "1.3.2"
+  resolved "https://registry.yarnpkg.com/engine.io-parser/-/engine.io-parser-1.3.2.tgz#937b079f0007d0893ec56d46cb220b8cb435220a"
+  dependencies:
+    after "0.8.2"
+    arraybuffer.slice "0.0.6"
+    base64-arraybuffer "0.1.5"
+    blob "0.0.4"
+    has-binary "0.1.7"
+    wtf-8 "1.0.0"
+
+engine.io@1.8.3:
+  version "1.8.3"
+  resolved "https://registry.yarnpkg.com/engine.io/-/engine.io-1.8.3.tgz#8de7f97895d20d39b85f88eeee777b2bd42b13d4"
+  dependencies:
+    accepts "1.3.3"
+    base64id "1.0.0"
+    cookie "0.3.1"
+    debug "2.3.3"
+    engine.io-parser "1.3.2"
+    ws "1.1.2"
+
+enhanced-resolve@^3.4.0:
+  version "3.4.1"
+  resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-3.4.1.tgz#0421e339fd71419b3da13d129b3979040230476e"
+  dependencies:
+    graceful-fs "^4.1.2"
+    memory-fs "^0.4.0"
+    object-assign "^4.0.1"
+    tapable "^0.2.7"
+
+ent@~2.2.0:
+  version "2.2.0"
+  resolved "https://registry.yarnpkg.com/ent/-/ent-2.2.0.tgz#e964219325a21d05f44466a2f686ed6ce5f5dd1d"
+
+entities@^1.1.1, entities@~1.1.1:
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/entities/-/entities-1.1.1.tgz#6e5c2d0a5621b5dadaecef80b90edfb5cd7772f0"
+
+errno@^0.1.3:
+  version "0.1.7"
+  resolved "https://registry.yarnpkg.com/errno/-/errno-0.1.7.tgz#4684d71779ad39af177e3f007996f7c67c852618"
+  dependencies:
+    prr "~1.0.1"
+
+error-ex@^1.2.0:
+  version "1.3.1"
+  resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.1.tgz#f855a86ce61adc4e8621c3cda21e7a7612c3a8dc"
+  dependencies:
+    is-arrayish "^0.2.1"
+
+error-stack-parser@^2.0.0:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/error-stack-parser/-/error-stack-parser-2.0.1.tgz#a3202b8fb03114aa9b40a0e3669e48b2b65a010a"
+  dependencies:
+    stackframe "^1.0.3"
+
+es5-ext@^0.10.14, es5-ext@^0.10.35, es5-ext@^0.10.9, es5-ext@~0.10.14:
+  version "0.10.42"
+  resolved "https://registry.yarnpkg.com/es5-ext/-/es5-ext-0.10.42.tgz#8c07dd33af04d5dcd1310b5cef13bea63a89ba8d"
+  dependencies:
+    es6-iterator "~2.0.3"
+    es6-symbol "~3.1.1"
+    next-tick "1"
+
+es6-iterator@^2.0.1, es6-iterator@~2.0.1, es6-iterator@~2.0.3:
+  version "2.0.3"
+  resolved "https://registry.yarnpkg.com/es6-iterator/-/es6-iterator-2.0.3.tgz#a7de889141a05a94b0854403b2d0a0fbfa98f3b7"
+  dependencies:
+    d "1"
+    es5-ext "^0.10.35"
+    es6-symbol "^3.1.1"
+
+es6-map@^0.1.3:
+  version "0.1.5"
+  resolved "https://registry.yarnpkg.com/es6-map/-/es6-map-0.1.5.tgz#9136e0503dcc06a301690f0bb14ff4e364e949f0"
+  dependencies:
+    d "1"
+    es5-ext "~0.10.14"
+    es6-iterator "~2.0.1"
+    es6-set "~0.1.5"
+    es6-symbol "~3.1.1"
+    event-emitter "~0.3.5"
+
+es6-promise@^4.0.3, es6-promise@^4.2.2:
+  version "4.2.4"
+  resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-4.2.4.tgz#dc4221c2b16518760bd8c39a52d8f356fc00ed29"
+
+es6-set@~0.1.5:
+  version "0.1.5"
+  resolved "https://registry.yarnpkg.com/es6-set/-/es6-set-0.1.5.tgz#d2b3ec5d4d800ced818db538d28974db0a73ccb1"
+  dependencies:
+    d "1"
+    es5-ext "~0.10.14"
+    es6-iterator "~2.0.1"
+    es6-symbol "3.1.1"
+    event-emitter "~0.3.5"
+
+es6-symbol@3.1.1, es6-symbol@^3.1.1, es6-symbol@~3.1.1:
+  version "3.1.1"
+  resolved "https://registry.yarnpkg.com/es6-symbol/-/es6-symbol-3.1.1.tgz#bf00ef4fdab6ba1b46ecb7b629b4c7ed5715cc77"
+  dependencies:
+    d "1"
+    es5-ext "~0.10.14"
+
+es6-weak-map@^2.0.1:
+  version "2.0.2"
+  resolved "https://registry.yarnpkg.com/es6-weak-map/-/es6-weak-map-2.0.2.tgz#5e3ab32251ffd1538a1f8e5ffa1357772f92d96f"
+  dependencies:
+    d "1"
+    es5-ext "^0.10.14"
+    es6-iterator "^2.0.1"
+    es6-symbol "^3.1.1"
+
+escape-html@~1.0.3:
+  version "1.0.3"
+  resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988"
+
+escape-string-regexp@1.0.5, escape-string-regexp@^1.0.2, escape-string-regexp@^1.0.5:
+  version "1.0.5"
+  resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4"
+
+escodegen@1.8.x:
+  version "1.8.1"
+  resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-1.8.1.tgz#5a5b53af4693110bebb0867aa3430dd3b70a1018"
+  dependencies:
+    esprima "^2.7.1"
+    estraverse "^1.9.1"
+    esutils "^2.0.2"
+    optionator "^0.8.1"
+  optionalDependencies:
+    source-map "~0.2.0"
+
+escodegen@1.x.x:
+  version "1.9.1"
+  resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-1.9.1.tgz#dbae17ef96c8e4bedb1356f4504fa4cc2f7cb7e2"
+  dependencies:
+    esprima "^3.1.3"
+    estraverse "^4.2.0"
+    esutils "^2.0.2"
+    optionator "^0.8.1"
+  optionalDependencies:
+    source-map "~0.6.1"
+
+escope@^3.6.0:
+  version "3.6.0"
+  resolved "https://registry.yarnpkg.com/escope/-/escope-3.6.0.tgz#e01975e812781a163a6dadfdd80398dc64c889c3"
+  dependencies:
+    es6-map "^0.1.3"
+    es6-weak-map "^2.0.1"
+    esrecurse "^4.1.0"
+    estraverse "^4.1.1"
+
+eslint-config-standard@^6.2.1:
+  version "6.2.1"
+  resolved "https://registry.yarnpkg.com/eslint-config-standard/-/eslint-config-standard-6.2.1.tgz#d3a68aafc7191639e7ee441e7348739026354292"
+
+eslint-friendly-formatter@^2.0.7:
+  version "2.0.7"
+  resolved "https://registry.yarnpkg.com/eslint-friendly-formatter/-/eslint-friendly-formatter-2.0.7.tgz#657f95a19af4989636afebb1cc9de6cebbd088ee"
+  dependencies:
+    chalk "^1.0.0"
+    extend "^3.0.0"
+    minimist "^1.2.0"
+    text-table "^0.2.0"
+
+eslint-loader@^1.7.1:
+  version "1.9.0"
+  resolved "https://registry.yarnpkg.com/eslint-loader/-/eslint-loader-1.9.0.tgz#7e1be9feddca328d3dcfaef1ad49d5beffe83a13"
+  dependencies:
+    loader-fs-cache "^1.0.0"
+    loader-utils "^1.0.2"
+    object-assign "^4.0.1"
+    object-hash "^1.1.4"
+    rimraf "^2.6.1"
+
+eslint-plugin-html@^2.0.0:
+  version "2.0.3"
+  resolved "https://registry.yarnpkg.com/eslint-plugin-html/-/eslint-plugin-html-2.0.3.tgz#7c89883ab0c85fa5d28b666a14a4e906aa90b897"
+  dependencies:
+    htmlparser2 "^3.8.2"
+
+eslint-plugin-promise@^3.4.0:
+  version "3.7.0"
+  resolved "https://registry.yarnpkg.com/eslint-plugin-promise/-/eslint-plugin-promise-3.7.0.tgz#f4bde5c2c77cdd69557a8f69a24d1ad3cfc9e67e"
+
+eslint-plugin-standard@^2.0.1:
+  version "2.3.1"
+  resolved "https://registry.yarnpkg.com/eslint-plugin-standard/-/eslint-plugin-standard-2.3.1.tgz#6765bd2a6d9ecdc7bdf1b145ae4bb30e2b7b86f8"
+
+eslint@^3.19.0:
+  version "3.19.0"
+  resolved "https://registry.yarnpkg.com/eslint/-/eslint-3.19.0.tgz#c8fc6201c7f40dd08941b87c085767386a679acc"
+  dependencies:
+    babel-code-frame "^6.16.0"
+    chalk "^1.1.3"
+    concat-stream "^1.5.2"
+    debug "^2.1.1"
+    doctrine "^2.0.0"
+    escope "^3.6.0"
+    espree "^3.4.0"
+    esquery "^1.0.0"
+    estraverse "^4.2.0"
+    esutils "^2.0.2"
+    file-entry-cache "^2.0.0"
+    glob "^7.0.3"
+    globals "^9.14.0"
+    ignore "^3.2.0"
+    imurmurhash "^0.1.4"
+    inquirer "^0.12.0"
+    is-my-json-valid "^2.10.0"
+    is-resolvable "^1.0.0"
+    js-yaml "^3.5.1"
+    json-stable-stringify "^1.0.0"
+    levn "^0.3.0"
+    lodash "^4.0.0"
+    mkdirp "^0.5.0"
+    natural-compare "^1.4.0"
+    optionator "^0.8.2"
+    path-is-inside "^1.0.1"
+    pluralize "^1.2.1"
+    progress "^1.1.8"
+    require-uncached "^1.0.2"
+    shelljs "^0.7.5"
+    strip-bom "^3.0.0"
+    strip-json-comments "~2.0.1"
+    table "^3.7.8"
+    text-table "~0.2.0"
+    user-home "^2.0.0"
+
+espree@^3.4.0:
+  version "3.5.4"
+  resolved "https://registry.yarnpkg.com/espree/-/espree-3.5.4.tgz#b0f447187c8a8bed944b815a660bddf5deb5d1a7"
+  dependencies:
+    acorn "^5.5.0"
+    acorn-jsx "^3.0.0"
+
+esprima@2.7.x, esprima@^2.6.0, esprima@^2.7.1:
+  version "2.7.3"
+  resolved "https://registry.yarnpkg.com/esprima/-/esprima-2.7.3.tgz#96e3b70d5779f6ad49cd032673d1c312767ba581"
+
+esprima@3.x.x, esprima@^3.1.3:
+  version "3.1.3"
+  resolved "https://registry.yarnpkg.com/esprima/-/esprima-3.1.3.tgz#fdca51cee6133895e3c88d535ce49dbff62a4633"
+
+esprima@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.0.tgz#4499eddcd1110e0b218bacf2fa7f7f59f55ca804"
+
+esquery@^1.0.0:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.0.1.tgz#406c51658b1f5991a5f9b62b1dc25b00e3e5c708"
+  dependencies:
+    estraverse "^4.0.0"
+
+esrecurse@^4.1.0:
+  version "4.2.1"
+  resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.2.1.tgz#007a3b9fdbc2b3bb87e4879ea19c92fdbd3942cf"
+  dependencies:
+    estraverse "^4.1.0"
+
+estraverse@^1.9.1:
+  version "1.9.3"
+  resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-1.9.3.tgz#af67f2dc922582415950926091a4005d29c9bb44"
+
+estraverse@^4.0.0, estraverse@^4.1.0, estraverse@^4.1.1, estraverse@^4.2.0:
+  version "4.2.0"
+  resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.2.0.tgz#0dee3fed31fcd469618ce7342099fc1afa0bdb13"
+
+esutils@^2.0.2:
+  version "2.0.2"
+  resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.2.tgz#0abf4f1caa5bcb1f7a9d8acc6dea4faaa04bac9b"
+
+etag@~1.8.1:
+  version "1.8.1"
+  resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887"
+
+ev-emitter@^1.0.0:
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/ev-emitter/-/ev-emitter-1.1.1.tgz#8f18b0ce5c76a5d18017f71c0a795c65b9138f2a"
+
+event-emitter@~0.3.5:
+  version "0.3.5"
+  resolved "https://registry.yarnpkg.com/event-emitter/-/event-emitter-0.3.5.tgz#df8c69eef1647923c7157b9ce83840610b02cc39"
+  dependencies:
+    d "1"
+    es5-ext "~0.10.14"
+
+eventemitter3@1.x.x:
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-1.2.0.tgz#1c86991d816ad1e504750e73874224ecf3bec508"
+
+events@^1.0.0:
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/events/-/events-1.1.1.tgz#9ebdb7635ad099c70dcc4c2a1f5004288e8bd924"
+
+eventsource-polyfill@^0.9.6:
+  version "0.9.6"
+  resolved "https://registry.yarnpkg.com/eventsource-polyfill/-/eventsource-polyfill-0.9.6.tgz#10e0d187f111b167f28fdab918843ce7d818f13c"
+
+evp_bytestokey@^1.0.0, evp_bytestokey@^1.0.3:
+  version "1.0.3"
+  resolved "https://registry.yarnpkg.com/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz#7fcbdb198dc71959432efe13842684e0525acb02"
+  dependencies:
+    md5.js "^1.3.4"
+    safe-buffer "^5.1.1"
+
+execa@^0.7.0:
+  version "0.7.0"
+  resolved "https://registry.yarnpkg.com/execa/-/execa-0.7.0.tgz#944becd34cc41ee32a63a9faf27ad5a65fc59777"
+  dependencies:
+    cross-spawn "^5.0.1"
+    get-stream "^3.0.0"
+    is-stream "^1.1.0"
+    npm-run-path "^2.0.0"
+    p-finally "^1.0.0"
+    signal-exit "^3.0.0"
+    strip-eof "^1.0.0"
+
+exit-hook@^1.0.0:
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/exit-hook/-/exit-hook-1.1.1.tgz#f05ca233b48c05d54fff07765df8507e95c02ff8"
+
+expand-braces@^0.1.1:
+  version "0.1.2"
+  resolved "https://registry.yarnpkg.com/expand-braces/-/expand-braces-0.1.2.tgz#488b1d1d2451cb3d3a6b192cfc030f44c5855fea"
+  dependencies:
+    array-slice "^0.2.3"
+    array-unique "^0.2.1"
+    braces "^0.1.2"
+
+expand-brackets@^0.1.4:
+  version "0.1.5"
+  resolved "https://registry.yarnpkg.com/expand-brackets/-/expand-brackets-0.1.5.tgz#df07284e342a807cd733ac5af72411e581d1177b"
+  dependencies:
+    is-posix-bracket "^0.1.0"
+
+expand-brackets@^2.1.4:
+  version "2.1.4"
+  resolved "https://registry.yarnpkg.com/expand-brackets/-/expand-brackets-2.1.4.tgz#b77735e315ce30f6b6eff0f83b04151a22449622"
+  dependencies:
+    debug "^2.3.3"
+    define-property "^0.2.5"
+    extend-shallow "^2.0.1"
+    posix-character-classes "^0.1.0"
+    regex-not "^1.0.0"
+    snapdragon "^0.8.1"
+    to-regex "^3.0.1"
+
+expand-range@^0.1.0:
+  version "0.1.1"
+  resolved "https://registry.yarnpkg.com/expand-range/-/expand-range-0.1.1.tgz#4cb8eda0993ca56fa4f41fc42f3cbb4ccadff044"
+  dependencies:
+    is-number "^0.1.1"
+    repeat-string "^0.2.2"
+
+expand-range@^1.8.1:
+  version "1.8.2"
+  resolved "https://registry.yarnpkg.com/expand-range/-/expand-range-1.8.2.tgz#a299effd335fe2721ebae8e257ec79644fc85337"
+  dependencies:
+    fill-range "^2.1.0"
+
+express@^4.14.1, express@^4.16.2:
+  version "4.16.3"
+  resolved "https://registry.yarnpkg.com/express/-/express-4.16.3.tgz#6af8a502350db3246ecc4becf6b5a34d22f7ed53"
+  dependencies:
+    accepts "~1.3.5"
+    array-flatten "1.1.1"
+    body-parser "1.18.2"
+    content-disposition "0.5.2"
+    content-type "~1.0.4"
+    cookie "0.3.1"
+    cookie-signature "1.0.6"
+    debug "2.6.9"
+    depd "~1.1.2"
+    encodeurl "~1.0.2"
+    escape-html "~1.0.3"
+    etag "~1.8.1"
+    finalhandler "1.1.1"
+    fresh "0.5.2"
+    merge-descriptors "1.0.1"
+    methods "~1.1.2"
+    on-finished "~2.3.0"
+    parseurl "~1.3.2"
+    path-to-regexp "0.1.7"
+    proxy-addr "~2.0.3"
+    qs "6.5.1"
+    range-parser "~1.2.0"
+    safe-buffer "5.1.1"
+    send "0.16.2"
+    serve-static "1.13.2"
+    setprototypeof "1.1.0"
+    statuses "~1.4.0"
+    type-is "~1.6.16"
+    utils-merge "1.0.1"
+    vary "~1.1.2"
+
+extend-shallow@^2.0.1:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/extend-shallow/-/extend-shallow-2.0.1.tgz#51af7d614ad9a9f610ea1bafbb989d6b1c56890f"
+  dependencies:
+    is-extendable "^0.1.0"
+
+extend-shallow@^3.0.0, extend-shallow@^3.0.2:
+  version "3.0.2"
+  resolved "https://registry.yarnpkg.com/extend-shallow/-/extend-shallow-3.0.2.tgz#26a71aaf073b39fb2127172746131c2704028db8"
+  dependencies:
+    assign-symbols "^1.0.0"
+    is-extendable "^1.0.1"
+
+extend@3, extend@^3.0.0, extend@~3.0.0, extend@~3.0.1:
+  version "3.0.1"
+  resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.1.tgz#a755ea7bc1adfcc5a31ce7e762dbaadc5e636444"
+
+extglob@^0.3.1:
+  version "0.3.2"
+  resolved "https://registry.yarnpkg.com/extglob/-/extglob-0.3.2.tgz#2e18ff3d2f49ab2765cec9023f011daa8d8349a1"
+  dependencies:
+    is-extglob "^1.0.0"
+
+extglob@^2.0.4:
+  version "2.0.4"
+  resolved "https://registry.yarnpkg.com/extglob/-/extglob-2.0.4.tgz#ad00fe4dc612a9232e8718711dc5cb5ab0285543"
+  dependencies:
+    array-unique "^0.3.2"
+    define-property "^1.0.0"
+    expand-brackets "^2.1.4"
+    extend-shallow "^2.0.1"
+    fragment-cache "^0.2.1"
+    regex-not "^1.0.0"
+    snapdragon "^0.8.1"
+    to-regex "^3.0.1"
+
+extract-text-webpack-plugin@^2.0.0:
+  version "2.1.2"
+  resolved "https://registry.yarnpkg.com/extract-text-webpack-plugin/-/extract-text-webpack-plugin-2.1.2.tgz#756ef4efa8155c3681833fbc34da53b941746d6c"
+  dependencies:
+    async "^2.1.2"
+    loader-utils "^1.0.2"
+    schema-utils "^0.3.0"
+    webpack-sources "^1.0.1"
+
+extract-zip@^1.6.5:
+  version "1.6.6"
+  resolved "https://registry.yarnpkg.com/extract-zip/-/extract-zip-1.6.6.tgz#1290ede8d20d0872b429fd3f351ca128ec5ef85c"
+  dependencies:
+    concat-stream "1.6.0"
+    debug "2.6.9"
+    mkdirp "0.5.0"
+    yauzl "2.4.1"
+
+extsprintf@1.3.0:
+  version "1.3.0"
+  resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.3.0.tgz#96918440e3041a7a414f8c52e3c574eb3c3e1e05"
+
+extsprintf@^1.2.0:
+  version "1.4.0"
+  resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.4.0.tgz#e2689f8f356fad62cca65a3a91c5df5f9551692f"
+
+fast-deep-equal@^1.0.0:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-1.1.0.tgz#c053477817c86b51daa853c81e059b733d023614"
+
+fast-json-stable-stringify@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz#d5142c0caee6b1189f87d3a76111064f86c8bbf2"
+
+fast-levenshtein@~2.0.4:
+  version "2.0.6"
+  resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917"
+
+fastparse@^1.1.1:
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/fastparse/-/fastparse-1.1.1.tgz#d1e2643b38a94d7583b479060e6c4affc94071f8"
+
+fd-slicer@~1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/fd-slicer/-/fd-slicer-1.0.1.tgz#8b5bcbd9ec327c5041bf9ab023fd6750f1177e65"
+  dependencies:
+    pend "~1.2.0"
+
+figures@^1.3.5:
+  version "1.7.0"
+  resolved "https://registry.yarnpkg.com/figures/-/figures-1.7.0.tgz#cbe1e3affcf1cd44b80cadfed28dc793a9701d2e"
+  dependencies:
+    escape-string-regexp "^1.0.5"
+    object-assign "^4.1.0"
+
+file-entry-cache@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-2.0.0.tgz#c392990c3e684783d838b8c84a45d8a048458361"
+  dependencies:
+    flat-cache "^1.2.1"
+    object-assign "^4.0.1"
+
+file-loader@^0.11.1:
+  version "0.11.2"
+  resolved "https://registry.yarnpkg.com/file-loader/-/file-loader-0.11.2.tgz#4ff1df28af38719a6098093b88c82c71d1794a34"
+  dependencies:
+    loader-utils "^1.0.2"
+
+file-uri-to-path@1:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz#553a7b8446ff6f684359c445f1e37a05dacc33dd"
+
+filename-regex@^2.0.0:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/filename-regex/-/filename-regex-2.0.1.tgz#c1c4b9bee3e09725ddb106b75c1e301fe2f18b26"
+
+filesize@^3.5.11:
+  version "3.6.1"
+  resolved "https://registry.yarnpkg.com/filesize/-/filesize-3.6.1.tgz#090bb3ee01b6f801a8a8be99d31710b3422bb317"
+
+fill-range@^2.1.0:
+  version "2.2.3"
+  resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-2.2.3.tgz#50b77dfd7e469bc7492470963699fe7a8485a723"
+  dependencies:
+    is-number "^2.1.0"
+    isobject "^2.0.0"
+    randomatic "^1.1.3"
+    repeat-element "^1.1.2"
+    repeat-string "^1.5.2"
+
+fill-range@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-4.0.0.tgz#d544811d428f98eb06a63dc402d2403c328c38f7"
+  dependencies:
+    extend-shallow "^2.0.1"
+    is-number "^3.0.0"
+    repeat-string "^1.6.1"
+    to-regex-range "^2.1.0"
+
+finalhandler@1.1.0:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.1.0.tgz#ce0b6855b45853e791b2fcc680046d88253dd7f5"
+  dependencies:
+    debug "2.6.9"
+    encodeurl "~1.0.1"
+    escape-html "~1.0.3"
+    on-finished "~2.3.0"
+    parseurl "~1.3.2"
+    statuses "~1.3.1"
+    unpipe "~1.0.0"
+
+finalhandler@1.1.1:
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.1.1.tgz#eebf4ed840079c83f4249038c9d703008301b105"
+  dependencies:
+    debug "2.6.9"
+    encodeurl "~1.0.2"
+    escape-html "~1.0.3"
+    on-finished "~2.3.0"
+    parseurl "~1.3.2"
+    statuses "~1.4.0"
+    unpipe "~1.0.0"
+
+find-cache-dir@^0.1.1:
+  version "0.1.1"
+  resolved "https://registry.yarnpkg.com/find-cache-dir/-/find-cache-dir-0.1.1.tgz#c8defae57c8a52a8a784f9e31c57c742e993a0b9"
+  dependencies:
+    commondir "^1.0.1"
+    mkdirp "^0.5.1"
+    pkg-dir "^1.0.0"
+
+find-cache-dir@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/find-cache-dir/-/find-cache-dir-1.0.0.tgz#9288e3e9e3cc3748717d39eade17cf71fc30ee6f"
+  dependencies:
+    commondir "^1.0.1"
+    make-dir "^1.0.0"
+    pkg-dir "^2.0.0"
+
+find-up@^1.0.0:
+  version "1.1.2"
+  resolved "https://registry.yarnpkg.com/find-up/-/find-up-1.1.2.tgz#6b2e9822b1a2ce0a60ab64d610eccad53cb24d0f"
+  dependencies:
+    path-exists "^2.0.0"
+    pinkie-promise "^2.0.0"
+
+find-up@^2.0.0, find-up@^2.1.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/find-up/-/find-up-2.1.0.tgz#45d1b7e506c717ddd482775a2b77920a3c0c57a7"
+  dependencies:
+    locate-path "^2.0.0"
+
+fizzy-ui-utils@^2.0.0:
+  version "2.0.7"
+  resolved "https://registry.yarnpkg.com/fizzy-ui-utils/-/fizzy-ui-utils-2.0.7.tgz#7df45dcc4eb374a08b65d39bb9a4beedf7330505"
+  dependencies:
+    desandro-matches-selector "^2.0.0"
+
+flat-cache@^1.2.1:
+  version "1.3.0"
+  resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-1.3.0.tgz#d3030b32b38154f4e3b7e9c709f490f7ef97c481"
+  dependencies:
+    circular-json "^0.3.1"
+    del "^2.0.2"
+    graceful-fs "^4.1.2"
+    write "^0.2.1"
+
+flatten@^1.0.2:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/flatten/-/flatten-1.0.2.tgz#dae46a9d78fbe25292258cc1e780a41d95c03782"
+
+flush-write-stream@^1.0.0:
+  version "1.0.3"
+  resolved "https://registry.yarnpkg.com/flush-write-stream/-/flush-write-stream-1.0.3.tgz#c5d586ef38af6097650b49bc41b55fabb19f35bd"
+  dependencies:
+    inherits "^2.0.1"
+    readable-stream "^2.0.4"
+
+follow-redirects@^1.2.5:
+  version "1.4.1"
+  resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.4.1.tgz#d8120f4518190f55aac65bb6fc7b85fcd666d6aa"
+  dependencies:
+    debug "^3.1.0"
+
+for-in@^0.1.3:
+  version "0.1.8"
+  resolved "https://registry.yarnpkg.com/for-in/-/for-in-0.1.8.tgz#d8773908e31256109952b1fdb9b3fa867d2775e1"
+
+for-in@^1.0.1, for-in@^1.0.2:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80"
+
+for-own@^0.1.4:
+  version "0.1.5"
+  resolved "https://registry.yarnpkg.com/for-own/-/for-own-0.1.5.tgz#5265c681a4f294dabbf17c9509b6763aa84510ce"
+  dependencies:
+    for-in "^1.0.1"
+
+for-own@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/for-own/-/for-own-1.0.0.tgz#c63332f415cedc4b04dbfe70cf836494c53cb44b"
+  dependencies:
+    for-in "^1.0.1"
+
+forever-agent@~0.6.1:
+  version "0.6.1"
+  resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91"
+
+form-data@~2.1.1:
+  version "2.1.4"
+  resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.1.4.tgz#33c183acf193276ecaa98143a69e94bfee1750d1"
+  dependencies:
+    asynckit "^0.4.0"
+    combined-stream "^1.0.5"
+    mime-types "^2.1.12"
+
+form-data@~2.3.1:
+  version "2.3.2"
+  resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.2.tgz#4970498be604c20c005d4f5c23aecd21d6b49099"
+  dependencies:
+    asynckit "^0.4.0"
+    combined-stream "1.0.6"
+    mime-types "^2.1.12"
+
+formatio@1.2.0:
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/formatio/-/formatio-1.2.0.tgz#f3b2167d9068c4698a8d51f4f760a39a54d818eb"
+  dependencies:
+    samsam "1.x"
+
+forwarded@~0.1.2:
+  version "0.1.2"
+  resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.1.2.tgz#98c23dab1175657b8c0573e8ceccd91b0ff18c84"
+
+fragment-cache@^0.2.1:
+  version "0.2.1"
+  resolved "https://registry.yarnpkg.com/fragment-cache/-/fragment-cache-0.2.1.tgz#4290fad27f13e89be7f33799c6bc5a0abfff0d19"
+  dependencies:
+    map-cache "^0.2.2"
+
+fresh@0.5.2:
+  version "0.5.2"
+  resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7"
+
+friendly-errors-webpack-plugin@^1.1.3:
+  version "1.7.0"
+  resolved "https://registry.yarnpkg.com/friendly-errors-webpack-plugin/-/friendly-errors-webpack-plugin-1.7.0.tgz#efc86cbb816224565861a1be7a9d84d0aafea136"
+  dependencies:
+    chalk "^1.1.3"
+    error-stack-parser "^2.0.0"
+    string-width "^2.0.0"
+
+from2@^2.1.0:
+  version "2.3.0"
+  resolved "https://registry.yarnpkg.com/from2/-/from2-2.3.0.tgz#8bfb5502bde4a4d36cfdeea007fcca21d7e382af"
+  dependencies:
+    inherits "^2.0.1"
+    readable-stream "^2.0.0"
+
+fs-extra@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-1.0.0.tgz#cd3ce5f7e7cb6145883fcae3191e9877f8587950"
+  dependencies:
+    graceful-fs "^4.1.2"
+    jsonfile "^2.1.0"
+    klaw "^1.0.0"
+
+fs-write-stream-atomic@^1.0.8:
+  version "1.0.10"
+  resolved "https://registry.yarnpkg.com/fs-write-stream-atomic/-/fs-write-stream-atomic-1.0.10.tgz#b47df53493ef911df75731e70a9ded0189db40c9"
+  dependencies:
+    graceful-fs "^4.1.2"
+    iferr "^0.1.5"
+    imurmurhash "^0.1.4"
+    readable-stream "1 || 2"
+
+fs.realpath@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"
+
+fs@0.0.1-security:
+  version "0.0.1-security"
+  resolved "https://registry.yarnpkg.com/fs/-/fs-0.0.1-security.tgz#8a7bd37186b6dddf3813f23858b57ecaaf5e41d4"
+
+fsevents@^1.0.0, fsevents@^1.1.2:
+  version "1.1.3"
+  resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-1.1.3.tgz#11f82318f5fe7bb2cd22965a108e9306208216d8"
+  dependencies:
+    nan "^2.3.0"
+    node-pre-gyp "^0.6.39"
+
+fstream-ignore@^1.0.5:
+  version "1.0.5"
+  resolved "https://registry.yarnpkg.com/fstream-ignore/-/fstream-ignore-1.0.5.tgz#9c31dae34767018fe1d249b24dada67d092da105"
+  dependencies:
+    fstream "^1.0.0"
+    inherits "2"
+    minimatch "^3.0.0"
+
+fstream@^1.0.0, fstream@^1.0.10, fstream@^1.0.2:
+  version "1.0.11"
+  resolved "https://registry.yarnpkg.com/fstream/-/fstream-1.0.11.tgz#5c1fb1f117477114f0632a0eb4b71b3cb0fd3171"
+  dependencies:
+    graceful-fs "^4.1.2"
+    inherits "~2.0.0"
+    mkdirp ">=0.5 0"
+    rimraf "2"
+
+ftp@~0.3.10:
+  version "0.3.10"
+  resolved "https://registry.yarnpkg.com/ftp/-/ftp-0.3.10.tgz#9197d861ad8142f3e63d5a83bfe4c59f7330885d"
+  dependencies:
+    readable-stream "1.1.x"
+    xregexp "2.0.0"
+
+function-bind@^1.0.2:
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d"
+
+gauge@~2.7.3:
+  version "2.7.4"
+  resolved "https://registry.yarnpkg.com/gauge/-/gauge-2.7.4.tgz#2c03405c7538c39d7eb37b317022e325fb018bf7"
+  dependencies:
+    aproba "^1.0.3"
+    console-control-strings "^1.0.0"
+    has-unicode "^2.0.0"
+    object-assign "^4.1.0"
+    signal-exit "^3.0.0"
+    string-width "^1.0.1"
+    strip-ansi "^3.0.1"
+    wide-align "^1.1.0"
+
+gaze@^1.0.0:
+  version "1.1.2"
+  resolved "https://registry.yarnpkg.com/gaze/-/gaze-1.1.2.tgz#847224677adb8870d679257ed3388fdb61e40105"
+  dependencies:
+    globule "^1.0.0"
+
+generate-function@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/generate-function/-/generate-function-2.0.0.tgz#6858fe7c0969b7d4e9093337647ac79f60dfbe74"
+
+generate-object-property@^1.1.0:
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/generate-object-property/-/generate-object-property-1.2.0.tgz#9c0e1c40308ce804f4783618b937fa88f99d50d0"
+  dependencies:
+    is-property "^1.0.0"
+
+get-caller-file@^1.0.1:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-1.0.2.tgz#f702e63127e7e231c160a80c1554acb70d5047e5"
+
+get-size@^2.0.2:
+  version "2.0.3"
+  resolved "https://registry.yarnpkg.com/get-size/-/get-size-2.0.3.tgz#54a1d0256b20ea7ac646516756202769941ad2ef"
+
+get-stdin@^4.0.1:
+  version "4.0.1"
+  resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-4.0.1.tgz#b968c6b0a04384324902e8bf1a5df32579a450fe"
+
+get-stream@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-3.0.0.tgz#8e943d1358dc37555054ecbe2edb05aa174ede14"
+
+get-uri@2:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/get-uri/-/get-uri-2.0.1.tgz#dbdcacacd8c608a38316869368117697a1631c59"
+  dependencies:
+    data-uri-to-buffer "1"
+    debug "2"
+    extend "3"
+    file-uri-to-path "1"
+    ftp "~0.3.10"
+    readable-stream "2"
+
+get-value@^2.0.3, get-value@^2.0.6:
+  version "2.0.6"
+  resolved "https://registry.yarnpkg.com/get-value/-/get-value-2.0.6.tgz#dc15ca1c672387ca76bd37ac0a395ba2042a2c28"
+
+getpass@^0.1.1:
+  version "0.1.7"
+  resolved "https://registry.yarnpkg.com/getpass/-/getpass-0.1.7.tgz#5eff8e3e684d569ae4cb2b1282604e8ba62149fa"
+  dependencies:
+    assert-plus "^1.0.0"
+
+gettext-parser@^1.3.0:
+  version "1.3.1"
+  resolved "https://registry.yarnpkg.com/gettext-parser/-/gettext-parser-1.3.1.tgz#74b7a99e4b5fa8daab11fa515e8a582480448a12"
+  dependencies:
+    encoding "^0.1.12"
+    safe-buffer "^5.1.1"
+
+glob-base@^0.3.0:
+  version "0.3.0"
+  resolved "https://registry.yarnpkg.com/glob-base/-/glob-base-0.3.0.tgz#dbb164f6221b1c0b1ccf82aea328b497df0ea3c4"
+  dependencies:
+    glob-parent "^2.0.0"
+    is-glob "^2.0.0"
+
+glob-parent@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-2.0.0.tgz#81383d72db054fcccf5336daa902f182f6edbb28"
+  dependencies:
+    is-glob "^2.0.0"
+
+glob-parent@^3.1.0:
+  version "3.1.0"
+  resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-3.1.0.tgz#9e6af6299d8d3bd2bd40430832bd113df906c5ae"
+  dependencies:
+    is-glob "^3.1.0"
+    path-dirname "^1.0.0"
+
+glob@7.0.5:
+  version "7.0.5"
+  resolved "https://registry.yarnpkg.com/glob/-/glob-7.0.5.tgz#b4202a69099bbb4d292a7c1b95b6682b67ebdc95"
+  dependencies:
+    fs.realpath "^1.0.0"
+    inflight "^1.0.4"
+    inherits "2"
+    minimatch "^3.0.2"
+    once "^1.3.0"
+    path-is-absolute "^1.0.0"
+
+glob@7.1.1:
+  version "7.1.1"
+  resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.1.tgz#805211df04faaf1c63a3600306cdf5ade50b2ec8"
+  dependencies:
+    fs.realpath "^1.0.0"
+    inflight "^1.0.4"
+    inherits "2"
+    minimatch "^3.0.2"
+    once "^1.3.0"
+    path-is-absolute "^1.0.0"
+
+glob@^5.0.15:
+  version "5.0.15"
+  resolved "https://registry.yarnpkg.com/glob/-/glob-5.0.15.tgz#1bc936b9e02f4a603fcc222ecf7633d30b8b93b1"
+  dependencies:
+    inflight "^1.0.4"
+    inherits "2"
+    minimatch "2 || 3"
+    once "^1.3.0"
+    path-is-absolute "^1.0.0"
+
+glob@^6.0.4:
+  version "6.0.4"
+  resolved "https://registry.yarnpkg.com/glob/-/glob-6.0.4.tgz#0f08860f6a155127b2fadd4f9ce24b1aab6e4d22"
+  dependencies:
+    inflight "^1.0.4"
+    inherits "2"
+    minimatch "2 || 3"
+    once "^1.3.0"
+    path-is-absolute "^1.0.0"
+
+glob@^7.0.0, glob@^7.0.3, glob@^7.0.5, glob@^7.1.1, glob@^7.1.2, glob@~7.1.1:
+  version "7.1.2"
+  resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.2.tgz#c19c9df9a028702d678612384a6552404c636d15"
+  dependencies:
+    fs.realpath "^1.0.0"
+    inflight "^1.0.4"
+    inherits "2"
+    minimatch "^3.0.4"
+    once "^1.3.0"
+    path-is-absolute "^1.0.0"
+
+globals@^9.14.0, globals@^9.18.0:
+  version "9.18.0"
+  resolved "https://registry.yarnpkg.com/globals/-/globals-9.18.0.tgz#aa3896b3e69b487f17e31ed2143d69a8e30c2d8a"
+
+globby@^5.0.0:
+  version "5.0.0"
+  resolved "https://registry.yarnpkg.com/globby/-/globby-5.0.0.tgz#ebd84667ca0dbb330b99bcfc68eac2bc54370e0d"
+  dependencies:
+    array-union "^1.0.1"
+    arrify "^1.0.0"
+    glob "^7.0.3"
+    object-assign "^4.0.1"
+    pify "^2.0.0"
+    pinkie-promise "^2.0.0"
+
+globby@^6.1.0:
+  version "6.1.0"
+  resolved "https://registry.yarnpkg.com/globby/-/globby-6.1.0.tgz#f5a6d70e8395e21c858fb0489d64df02424d506c"
+  dependencies:
+    array-union "^1.0.1"
+    glob "^7.0.3"
+    object-assign "^4.0.1"
+    pify "^2.0.0"
+    pinkie-promise "^2.0.0"
+
+globby@^7.1.1:
+  version "7.1.1"
+  resolved "https://registry.yarnpkg.com/globby/-/globby-7.1.1.tgz#fb2ccff9401f8600945dfada97440cca972b8680"
+  dependencies:
+    array-union "^1.0.1"
+    dir-glob "^2.0.0"
+    glob "^7.1.2"
+    ignore "^3.3.5"
+    pify "^3.0.0"
+    slash "^1.0.0"
+
+globule@^1.0.0:
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/globule/-/globule-1.2.0.tgz#1dc49c6822dd9e8a2fa00ba2a295006e8664bd09"
+  dependencies:
+    glob "~7.1.1"
+    lodash "~4.17.4"
+    minimatch "~3.0.2"
+
+graceful-fs@^4.1.11, graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.1.9:
+  version "4.1.11"
+  resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.11.tgz#0e8bdfe4d1ddb8854d64e04ea7c00e2a026e5658"
+
+"graceful-readlink@>= 1.0.0":
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/graceful-readlink/-/graceful-readlink-1.0.1.tgz#4cafad76bc62f02fa039b2f94e9a3dd3a391a725"
+
+growl@1.9.2:
+  version "1.9.2"
+  resolved "https://registry.yarnpkg.com/growl/-/growl-1.9.2.tgz#0ea7743715db8d8de2c5ede1775e1b45ac85c02f"
+
+gzip-size@^4.1.0:
+  version "4.1.0"
+  resolved "https://registry.yarnpkg.com/gzip-size/-/gzip-size-4.1.0.tgz#8ae096257eabe7d69c45be2b67c448124ffb517c"
+  dependencies:
+    duplexer "^0.1.1"
+    pify "^3.0.0"
+
+handlebars@^4.0.1:
+  version "4.0.11"
+  resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.0.11.tgz#630a35dfe0294bc281edae6ffc5d329fc7982dcc"
+  dependencies:
+    async "^1.4.0"
+    optimist "^0.6.1"
+    source-map "^0.4.4"
+  optionalDependencies:
+    uglify-js "^2.6"
+
+har-schema@^1.0.5:
+  version "1.0.5"
+  resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-1.0.5.tgz#d263135f43307c02c602afc8fe95970c0151369e"
+
+har-schema@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-2.0.0.tgz#a94c2224ebcac04782a0d9035521f24735b7ec92"
+
+har-validator@~2.0.6:
+  version "2.0.6"
+  resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-2.0.6.tgz#cdcbc08188265ad119b6a5a7c8ab70eecfb5d27d"
+  dependencies:
+    chalk "^1.1.1"
+    commander "^2.9.0"
+    is-my-json-valid "^2.12.4"
+    pinkie-promise "^2.0.0"
+
+har-validator@~4.2.1:
+  version "4.2.1"
+  resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-4.2.1.tgz#33481d0f1bbff600dd203d75812a6a5fba002e2a"
+  dependencies:
+    ajv "^4.9.1"
+    har-schema "^1.0.5"
+
+har-validator@~5.0.3:
+  version "5.0.3"
+  resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-5.0.3.tgz#ba402c266194f15956ef15e0fcf242993f6a7dfd"
+  dependencies:
+    ajv "^5.1.0"
+    har-schema "^2.0.0"
+
+has-ansi@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/has-ansi/-/has-ansi-2.0.0.tgz#34f5049ce1ecdf2b0649af3ef24e45ed35416d91"
+  dependencies:
+    ansi-regex "^2.0.0"
+
+has-binary@0.1.7:
+  version "0.1.7"
+  resolved "https://registry.yarnpkg.com/has-binary/-/has-binary-0.1.7.tgz#68e61eb16210c9545a0a5cce06a873912fe1e68c"
+  dependencies:
+    isarray "0.0.1"
+
+has-cors@1.1.0:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/has-cors/-/has-cors-1.1.0.tgz#5e474793f7ea9843d1bb99c23eef49ff126fff39"
+
+has-flag@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-1.0.0.tgz#9d9e793165ce017a00f00418c43f942a7b1d11fa"
+
+has-flag@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-2.0.0.tgz#e8207af1cc7b30d446cc70b734b5e8be18f88d51"
+
+has-flag@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd"
+
+has-unicode@^2.0.0:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/has-unicode/-/has-unicode-2.0.1.tgz#e0e6fe6a28cf51138855e086d1691e771de2a8b9"
+
+has-value@^0.3.1:
+  version "0.3.1"
+  resolved "https://registry.yarnpkg.com/has-value/-/has-value-0.3.1.tgz#7b1f58bada62ca827ec0a2078025654845995e1f"
+  dependencies:
+    get-value "^2.0.3"
+    has-values "^0.1.4"
+    isobject "^2.0.0"
+
+has-value@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/has-value/-/has-value-1.0.0.tgz#18b281da585b1c5c51def24c930ed29a0be6b177"
+  dependencies:
+    get-value "^2.0.6"
+    has-values "^1.0.0"
+    isobject "^3.0.0"
+
+has-values@^0.1.4:
+  version "0.1.4"
+  resolved "https://registry.yarnpkg.com/has-values/-/has-values-0.1.4.tgz#6d61de95d91dfca9b9a02089ad384bff8f62b771"
+
+has-values@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/has-values/-/has-values-1.0.0.tgz#95b0b63fec2146619a6fe57fe75628d5a39efe4f"
+  dependencies:
+    is-number "^3.0.0"
+    kind-of "^4.0.0"
+
+has@^1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/has/-/has-1.0.1.tgz#8461733f538b0837c9361e39a9ab9e9704dc2f28"
+  dependencies:
+    function-bind "^1.0.2"
+
+hash-base@^2.0.0:
+  version "2.0.2"
+  resolved "https://registry.yarnpkg.com/hash-base/-/hash-base-2.0.2.tgz#66ea1d856db4e8a5470cadf6fce23ae5244ef2e1"
+  dependencies:
+    inherits "^2.0.1"
+
+hash-base@^3.0.0:
+  version "3.0.4"
+  resolved "https://registry.yarnpkg.com/hash-base/-/hash-base-3.0.4.tgz#5fc8686847ecd73499403319a6b0a3f3f6ae4918"
+  dependencies:
+    inherits "^2.0.1"
+    safe-buffer "^5.0.1"
+
+hash-sum@^1.0.2:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/hash-sum/-/hash-sum-1.0.2.tgz#33b40777754c6432573c120cc3808bbd10d47f04"
+
+hash.js@^1.0.0, hash.js@^1.0.3:
+  version "1.1.3"
+  resolved "https://registry.yarnpkg.com/hash.js/-/hash.js-1.1.3.tgz#340dedbe6290187151c1ea1d777a3448935df846"
+  dependencies:
+    inherits "^2.0.3"
+    minimalistic-assert "^1.0.0"
+
+hasha@^2.2.0:
+  version "2.2.0"
+  resolved "https://registry.yarnpkg.com/hasha/-/hasha-2.2.0.tgz#78d7cbfc1e6d66303fe79837365984517b2f6ee1"
+  dependencies:
+    is-stream "^1.0.1"
+    pinkie-promise "^2.0.0"
+
+hawk@3.1.3, hawk@~3.1.3:
+  version "3.1.3"
+  resolved "https://registry.yarnpkg.com/hawk/-/hawk-3.1.3.tgz#078444bd7c1640b0fe540d2c9b73d59678e8e1c4"
+  dependencies:
+    boom "2.x.x"
+    cryptiles "2.x.x"
+    hoek "2.x.x"
+    sntp "1.x.x"
+
+hawk@~6.0.2:
+  version "6.0.2"
+  resolved "https://registry.yarnpkg.com/hawk/-/hawk-6.0.2.tgz#af4d914eb065f9b5ce4d9d11c1cb2126eecc3038"
+  dependencies:
+    boom "4.x.x"
+    cryptiles "3.x.x"
+    hoek "4.x.x"
+    sntp "2.x.x"
+
+he@1.1.1, he@1.1.x, he@^1.1.0:
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/he/-/he-1.1.1.tgz#93410fd21b009735151f8868c2f271f3427e23fd"
+
+hmac-drbg@^1.0.0:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/hmac-drbg/-/hmac-drbg-1.0.1.tgz#d2745701025a6c775a6c545793ed502fc0c649a1"
+  dependencies:
+    hash.js "^1.0.3"
+    minimalistic-assert "^1.0.0"
+    minimalistic-crypto-utils "^1.0.1"
+
+hoek@2.x.x:
+  version "2.16.3"
+  resolved "https://registry.yarnpkg.com/hoek/-/hoek-2.16.3.tgz#20bb7403d3cea398e91dc4710a8ff1b8274a25ed"
+
+hoek@4.x.x:
+  version "4.2.1"
+  resolved "https://registry.yarnpkg.com/hoek/-/hoek-4.2.1.tgz#9634502aa12c445dd5a7c5734b572bb8738aacbb"
+
+home-or-tmp@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/home-or-tmp/-/home-or-tmp-2.0.0.tgz#e36c3f2d2cae7d746a857e38d18d5f32a7882db8"
+  dependencies:
+    os-homedir "^1.0.0"
+    os-tmpdir "^1.0.1"
+
+hosted-git-info@^2.1.4:
+  version "2.6.0"
+  resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.6.0.tgz#23235b29ab230c576aab0d4f13fc046b0b038222"
+
+html-comment-regex@^1.1.0:
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/html-comment-regex/-/html-comment-regex-1.1.1.tgz#668b93776eaae55ebde8f3ad464b307a4963625e"
+
+html-entities@^1.2.0:
+  version "1.2.1"
+  resolved "https://registry.yarnpkg.com/html-entities/-/html-entities-1.2.1.tgz#0df29351f0721163515dfb9e5543e5f6eed5162f"
+
+html-minifier@^3.2.3:
+  version "3.5.14"
+  resolved "https://registry.yarnpkg.com/html-minifier/-/html-minifier-3.5.14.tgz#88653b24b344274e3e3d7052f1541ebea054ac60"
+  dependencies:
+    camel-case "3.0.x"
+    clean-css "4.1.x"
+    commander "2.15.x"
+    he "1.1.x"
+    param-case "2.1.x"
+    relateurl "0.2.x"
+    uglify-js "3.3.x"
+
+html-webpack-plugin@^2.28.0:
+  version "2.30.1"
+  resolved "https://registry.yarnpkg.com/html-webpack-plugin/-/html-webpack-plugin-2.30.1.tgz#7f9c421b7ea91ec460f56527d78df484ee7537d5"
+  dependencies:
+    bluebird "^3.4.7"
+    html-minifier "^3.2.3"
+    loader-utils "^0.2.16"
+    lodash "^4.17.3"
+    pretty-error "^2.0.2"
+    toposort "^1.0.0"
+
+htmlparser2@^3.8.2, htmlparser2@^3.9.1:
+  version "3.9.2"
+  resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-3.9.2.tgz#1bdf87acca0f3f9e53fa4fcceb0f4b4cbb00b338"
+  dependencies:
+    domelementtype "^1.3.0"
+    domhandler "^2.3.0"
+    domutils "^1.5.1"
+    entities "^1.1.1"
+    inherits "^2.0.1"
+    readable-stream "^2.0.2"
+
+htmlparser2@~3.3.0:
+  version "3.3.0"
+  resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-3.3.0.tgz#cc70d05a59f6542e43f0e685c982e14c924a9efe"
+  dependencies:
+    domelementtype "1"
+    domhandler "2.1"
+    domutils "1.1"
+    readable-stream "1.0"
+
+http-errors@1.6.2:
+  version "1.6.2"
+  resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.6.2.tgz#0a002cc85707192a7e7946ceedc11155f60ec736"
+  dependencies:
+    depd "1.1.1"
+    inherits "2.0.3"
+    setprototypeof "1.0.3"
+    statuses ">= 1.3.1 < 2"
+
+http-errors@~1.6.2:
+  version "1.6.3"
+  resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.6.3.tgz#8b55680bb4be283a0b5bf4ea2e38580be1d9320d"
+  dependencies:
+    depd "~1.1.2"
+    inherits "2.0.3"
+    setprototypeof "1.1.0"
+    statuses ">= 1.4.0 < 2"
+
+http-proxy-agent@1:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/http-proxy-agent/-/http-proxy-agent-1.0.0.tgz#cc1ce38e453bf984a0f7702d2dd59c73d081284a"
+  dependencies:
+    agent-base "2"
+    debug "2"
+    extend "3"
+
+http-proxy-middleware@^0.17.3:
+  version "0.17.4"
+  resolved "https://registry.yarnpkg.com/http-proxy-middleware/-/http-proxy-middleware-0.17.4.tgz#642e8848851d66f09d4f124912846dbaeb41b833"
+  dependencies:
+    http-proxy "^1.16.2"
+    is-glob "^3.1.0"
+    lodash "^4.17.2"
+    micromatch "^2.3.11"
+
+http-proxy@^1.13.0, http-proxy@^1.16.2:
+  version "1.16.2"
+  resolved "https://registry.yarnpkg.com/http-proxy/-/http-proxy-1.16.2.tgz#06dff292952bf64dbe8471fa9df73066d4f37742"
+  dependencies:
+    eventemitter3 "1.x.x"
+    requires-port "1.x.x"
+
+http-signature@~1.1.0:
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.1.1.tgz#df72e267066cd0ac67fb76adf8e134a8fbcf91bf"
+  dependencies:
+    assert-plus "^0.2.0"
+    jsprim "^1.2.2"
+    sshpk "^1.7.0"
+
+http-signature@~1.2.0:
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.2.0.tgz#9aecd925114772f3d95b65a60abb8f7c18fbace1"
+  dependencies:
+    assert-plus "^1.0.0"
+    jsprim "^1.2.2"
+    sshpk "^1.7.0"
+
+https-browserify@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/https-browserify/-/https-browserify-1.0.0.tgz#ec06c10e0a34c0f2faf199f7fd7fc78fffd03c73"
+
+https-proxy-agent@1:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-1.0.0.tgz#35f7da6c48ce4ddbfa264891ac593ee5ff8671e6"
+  dependencies:
+    agent-base "2"
+    debug "2"
+    extend "3"
+
+i18next-conv@^6.0.0:
+  version "6.0.0"
+  resolved "https://registry.yarnpkg.com/i18next-conv/-/i18next-conv-6.0.0.tgz#875a27bfb069db894f7b0a1484e0052100bc9383"
+  dependencies:
+    bluebird "^3.5.1"
+    chalk "^2.1.0"
+    commander "^2.11.0"
+    gettext-parser "^1.3.0"
+    mkdirp "^0.5.1"
+    node-gettext "^2.0.0"
+
+i18next-fetch-backend@^0.1.0:
+  version "0.1.0"
+  resolved "https://registry.yarnpkg.com/i18next-fetch-backend/-/i18next-fetch-backend-0.1.0.tgz#18b67920d0e605e616f93bbdf897e59adf9c9c05"
+  dependencies:
+    i18next-xhr-backend "^1.4.3"
+
+i18next-xhr-backend@^1.4.3:
+  version "1.5.1"
+  resolved "https://registry.yarnpkg.com/i18next-xhr-backend/-/i18next-xhr-backend-1.5.1.tgz#50282610780c6a696d880dfa7f4ac1d01e8c3ad5"
+
+i18next@^11.1.1:
+  version "11.1.1"
+  resolved "https://registry.yarnpkg.com/i18next/-/i18next-11.1.1.tgz#df3a683542d7756a8aa8d6b884b61141239c394a"
+
+iconv-lite@0.4.19:
+  version "0.4.19"
+  resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.19.tgz#f7468f60135f5e5dad3399c0a81be9a1603a082b"
+
+iconv-lite@~0.4.13:
+  version "0.4.21"
+  resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.21.tgz#c47f8733d02171189ebc4a400f3218d348094798"
+  dependencies:
+    safer-buffer "^2.1.0"
+
+icss-replace-symbols@^1.1.0:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/icss-replace-symbols/-/icss-replace-symbols-1.1.0.tgz#06ea6f83679a7749e386cfe1fe812ae5db223ded"
+
+icss-utils@^2.1.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/icss-utils/-/icss-utils-2.1.0.tgz#83f0a0ec378bf3246178b6c2ad9136f135b1c962"
+  dependencies:
+    postcss "^6.0.1"
+
+ieee754@^1.1.4:
+  version "1.1.11"
+  resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.1.11.tgz#c16384ffe00f5b7835824e67b6f2bd44a5229455"
+
+iferr@^0.1.5:
+  version "0.1.5"
+  resolved "https://registry.yarnpkg.com/iferr/-/iferr-0.1.5.tgz#c60eed69e6d8fdb6b3104a1fcbca1c192dc5b501"
+
+ignore@^3.2.0, ignore@^3.3.5:
+  version "3.3.7"
+  resolved "https://registry.yarnpkg.com/ignore/-/ignore-3.3.7.tgz#612289bfb3c220e186a58118618d5be8c1bab021"
+
+imagesloaded@latest:
+  version "4.1.4"
+  resolved "https://registry.yarnpkg.com/imagesloaded/-/imagesloaded-4.1.4.tgz#1376efcd162bb768c34c3727ac89cc04051f3cc7"
+  dependencies:
+    ev-emitter "^1.0.0"
+
+imurmurhash@^0.1.4:
+  version "0.1.4"
+  resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea"
+
+in-publish@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/in-publish/-/in-publish-2.0.0.tgz#e20ff5e3a2afc2690320b6dc552682a9c7fadf51"
+
+indent-string@^2.1.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-2.1.0.tgz#8e2d48348742121b4a8218b7a137e9a52049dc80"
+  dependencies:
+    repeating "^2.0.0"
+
+indexes-of@^1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/indexes-of/-/indexes-of-1.0.1.tgz#f30f716c8e2bd346c7b67d3df3915566a7c05607"
+
+indexof@0.0.1:
+  version "0.0.1"
+  resolved "https://registry.yarnpkg.com/indexof/-/indexof-0.0.1.tgz#82dc336d232b9062179d05ab3293a66059fd435d"
+
+inflight@^1.0.4:
+  version "1.0.6"
+  resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9"
+  dependencies:
+    once "^1.3.0"
+    wrappy "1"
+
+inherits@2, inherits@2.0.3, inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.0, inherits@~2.0.1, inherits@~2.0.3:
+  version "2.0.3"
+  resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de"
+
+inherits@2.0.1:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.1.tgz#b17d08d326b4423e568eff719f91b0b1cbdf69f1"
+
+ini@^1.3.4, ini@~1.3.0:
+  version "1.3.5"
+  resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.5.tgz#eee25f56db1c9ec6085e0c22778083f596abf927"
+
+inject-loader@^3.0.0:
+  version "3.0.1"
+  resolved "https://registry.yarnpkg.com/inject-loader/-/inject-loader-3.0.1.tgz#844a4596150cf6b864c2f79806d40a2caee4c2c7"
+  dependencies:
+    babel-core "~6"
+
+inquirer@^0.12.0:
+  version "0.12.0"
+  resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-0.12.0.tgz#1ef2bfd63504df0bc75785fff8c2c41df12f077e"
+  dependencies:
+    ansi-escapes "^1.1.0"
+    ansi-regex "^2.0.0"
+    chalk "^1.0.0"
+    cli-cursor "^1.0.1"
+    cli-width "^2.0.0"
+    figures "^1.3.5"
+    lodash "^4.3.0"
+    readline2 "^1.0.1"
+    run-async "^0.1.0"
+    rx-lite "^3.1.2"
+    string-width "^1.0.1"
+    strip-ansi "^3.0.0"
+    through "^2.3.6"
+
+interpret@^1.0.0:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.1.0.tgz#7ed1b1410c6a0e0f78cf95d3b8440c63f78b8614"
+
+invariant@^2.2.2:
+  version "2.2.4"
+  resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6"
+  dependencies:
+    loose-envify "^1.0.0"
+
+invert-kv@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/invert-kv/-/invert-kv-1.0.0.tgz#104a8e4aaca6d3d8cd157a8ef8bfab2d7a3ffdb6"
+
+ip@1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/ip/-/ip-1.0.1.tgz#c7e356cdea225ae71b36d70f2e71a92ba4e42590"
+
+ip@^1.1.4:
+  version "1.1.5"
+  resolved "https://registry.yarnpkg.com/ip/-/ip-1.1.5.tgz#bdded70114290828c0a039e72ef25f5aaec4354a"
+
+ipaddr.js@1.6.0:
+  version "1.6.0"
+  resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.6.0.tgz#e3fa357b773da619f26e95f049d055c72796f86b"
+
+is-absolute-url@^2.0.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/is-absolute-url/-/is-absolute-url-2.1.0.tgz#50530dfb84fcc9aa7dbe7852e83a37b93b9f2aa6"
+
+is-accessor-descriptor@^0.1.6:
+  version "0.1.6"
+  resolved "https://registry.yarnpkg.com/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz#a9e12cb3ae8d876727eeef3843f8a0897b5c98d6"
+  dependencies:
+    kind-of "^3.0.2"
+
+is-accessor-descriptor@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz#169c2f6d3df1f992618072365c9b0ea1f6878656"
+  dependencies:
+    kind-of "^6.0.0"
+
+is-arrayish@^0.2.1:
+  version "0.2.1"
+  resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d"
+
+is-binary-path@^1.0.0:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-1.0.1.tgz#75f16642b480f187a711c814161fd3a4a7655898"
+  dependencies:
+    binary-extensions "^1.0.0"
+
+is-buffer@^1.1.5:
+  version "1.1.6"
+  resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be"
+
+is-builtin-module@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/is-builtin-module/-/is-builtin-module-1.0.0.tgz#540572d34f7ac3119f8f76c30cbc1b1e037affbe"
+  dependencies:
+    builtin-modules "^1.0.0"
+
+is-data-descriptor@^0.1.4:
+  version "0.1.4"
+  resolved "https://registry.yarnpkg.com/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz#0b5ee648388e2c860282e793f1856fec3f301b56"
+  dependencies:
+    kind-of "^3.0.2"
+
+is-data-descriptor@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz#d84876321d0e7add03990406abbbbd36ba9268c7"
+  dependencies:
+    kind-of "^6.0.0"
+
+is-descriptor@^0.1.0:
+  version "0.1.6"
+  resolved "https://registry.yarnpkg.com/is-descriptor/-/is-descriptor-0.1.6.tgz#366d8240dde487ca51823b1ab9f07a10a78251ca"
+  dependencies:
+    is-accessor-descriptor "^0.1.6"
+    is-data-descriptor "^0.1.4"
+    kind-of "^5.0.0"
+
+is-descriptor@^1.0.0, is-descriptor@^1.0.2:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/is-descriptor/-/is-descriptor-1.0.2.tgz#3b159746a66604b04f8c81524ba365c5f14d86ec"
+  dependencies:
+    is-accessor-descriptor "^1.0.0"
+    is-data-descriptor "^1.0.0"
+    kind-of "^6.0.2"
+
+is-directory@^0.3.1:
+  version "0.3.1"
+  resolved "https://registry.yarnpkg.com/is-directory/-/is-directory-0.3.1.tgz#61339b6f2475fc772fd9c9d83f5c8575dc154ae1"
+
+is-dotfile@^1.0.0:
+  version "1.0.3"
+  resolved "https://registry.yarnpkg.com/is-dotfile/-/is-dotfile-1.0.3.tgz#a6a2f32ffd2dfb04f5ca25ecd0f6b83cf798a1e1"
+
+is-equal-shallow@^0.1.3:
+  version "0.1.3"
+  resolved "https://registry.yarnpkg.com/is-equal-shallow/-/is-equal-shallow-0.1.3.tgz#2238098fc221de0bcfa5d9eac4c45d638aa1c534"
+  dependencies:
+    is-primitive "^2.0.0"
+
+is-extendable@^0.1.0, is-extendable@^0.1.1:
+  version "0.1.1"
+  resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-0.1.1.tgz#62b110e289a471418e3ec36a617d472e301dfc89"
+
+is-extendable@^1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-1.0.1.tgz#a7470f9e426733d81bd81e1155264e3a3507cab4"
+  dependencies:
+    is-plain-object "^2.0.4"
+
+is-extglob@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-1.0.0.tgz#ac468177c4943405a092fc8f29760c6ffc6206c0"
+
+is-extglob@^2.1.0, is-extglob@^2.1.1:
+  version "2.1.1"
+  resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2"
+
+is-finite@^1.0.0:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/is-finite/-/is-finite-1.0.2.tgz#cc6677695602be550ef11e8b4aa6305342b6d0aa"
+  dependencies:
+    number-is-nan "^1.0.0"
+
+is-fullwidth-code-point@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz#ef9e31386f031a7f0d643af82fde50c457ef00cb"
+  dependencies:
+    number-is-nan "^1.0.0"
+
+is-fullwidth-code-point@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz#a3b30a5c4f199183167aaab93beefae3ddfb654f"
+
+is-glob@^2.0.0, is-glob@^2.0.1:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-2.0.1.tgz#d096f926a3ded5600f3fdfd91198cb0888c2d863"
+  dependencies:
+    is-extglob "^1.0.0"
+
+is-glob@^3.1.0:
+  version "3.1.0"
+  resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-3.1.0.tgz#7ba5ae24217804ac70707b96922567486cc3e84a"
+  dependencies:
+    is-extglob "^2.1.0"
+
+is-glob@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.0.tgz#9521c76845cc2610a85203ddf080a958c2ffabc0"
+  dependencies:
+    is-extglob "^2.1.1"
+
+is-my-ip-valid@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/is-my-ip-valid/-/is-my-ip-valid-1.0.0.tgz#7b351b8e8edd4d3995d4d066680e664d94696824"
+
+is-my-json-valid@^2.10.0, is-my-json-valid@^2.12.4:
+  version "2.17.2"
+  resolved "https://registry.yarnpkg.com/is-my-json-valid/-/is-my-json-valid-2.17.2.tgz#6b2103a288e94ef3de5cf15d29dd85fc4b78d65c"
+  dependencies:
+    generate-function "^2.0.0"
+    generate-object-property "^1.1.0"
+    is-my-ip-valid "^1.0.0"
+    jsonpointer "^4.0.0"
+    xtend "^4.0.0"
+
+is-number@^0.1.1:
+  version "0.1.1"
+  resolved "https://registry.yarnpkg.com/is-number/-/is-number-0.1.1.tgz#69a7af116963d47206ec9bd9b48a14216f1e3806"
+
+is-number@^2.1.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/is-number/-/is-number-2.1.0.tgz#01fcbbb393463a548f2f466cce16dece49db908f"
+  dependencies:
+    kind-of "^3.0.2"
+
+is-number@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/is-number/-/is-number-3.0.0.tgz#24fd6201a4782cf50561c810276afc7d12d71195"
+  dependencies:
+    kind-of "^3.0.2"
+
+is-number@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/is-number/-/is-number-4.0.0.tgz#0026e37f5454d73e356dfe6564699867c6a7f0ff"
+
+is-odd@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/is-odd/-/is-odd-2.0.0.tgz#7646624671fd7ea558ccd9a2795182f2958f1b24"
+  dependencies:
+    is-number "^4.0.0"
+
+is-path-cwd@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/is-path-cwd/-/is-path-cwd-1.0.0.tgz#d225ec23132e89edd38fda767472e62e65f1106d"
+
+is-path-in-cwd@^1.0.0:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/is-path-in-cwd/-/is-path-in-cwd-1.0.1.tgz#5ac48b345ef675339bd6c7a48a912110b241cf52"
+  dependencies:
+    is-path-inside "^1.0.0"
+
+is-path-inside@^1.0.0:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-1.0.1.tgz#8ef5b7de50437a3fdca6b4e865ef7aa55cb48036"
+  dependencies:
+    path-is-inside "^1.0.1"
+
+is-plain-obj@^1.0.0:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-1.1.0.tgz#71a50c8429dfca773c92a390a4a03b39fcd51d3e"
+
+is-plain-object@^2.0.1, is-plain-object@^2.0.3, is-plain-object@^2.0.4:
+  version "2.0.4"
+  resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-2.0.4.tgz#2c163b3fafb1b606d9d17928f05c2a1c38e07677"
+  dependencies:
+    isobject "^3.0.1"
+
+is-posix-bracket@^0.1.0:
+  version "0.1.1"
+  resolved "https://registry.yarnpkg.com/is-posix-bracket/-/is-posix-bracket-0.1.1.tgz#3334dc79774368e92f016e6fbc0a88f5cd6e6bc4"
+
+is-primitive@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/is-primitive/-/is-primitive-2.0.0.tgz#207bab91638499c07b2adf240a41a87210034575"
+
+is-property@^1.0.0:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/is-property/-/is-property-1.0.2.tgz#57fe1c4e48474edd65b09911f26b1cd4095dda84"
+
+is-resolvable@^1.0.0:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/is-resolvable/-/is-resolvable-1.1.0.tgz#fb18f87ce1feb925169c9a407c19318a3206ed88"
+
+is-stream@^1.0.1, is-stream@^1.1.0:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44"
+
+is-svg@^2.0.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/is-svg/-/is-svg-2.1.0.tgz#cf61090da0d9efbcab8722deba6f032208dbb0e9"
+  dependencies:
+    html-comment-regex "^1.1.0"
+
+is-typedarray@~1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a"
+
+is-utf8@^0.2.0:
+  version "0.2.1"
+  resolved "https://registry.yarnpkg.com/is-utf8/-/is-utf8-0.2.1.tgz#4b0da1442104d1b336340e80797e865cf39f7d72"
+
+is-windows@^1.0.0, is-windows@^1.0.2:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.2.tgz#d1850eb9791ecd18e6182ce12a30f396634bb19d"
+
+isarray@0.0.1:
+  version "0.0.1"
+  resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf"
+
+isarray@1.0.0, isarray@^1.0.0, isarray@~1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11"
+
+isbinaryfile@^3.0.0:
+  version "3.0.2"
+  resolved "https://registry.yarnpkg.com/isbinaryfile/-/isbinaryfile-3.0.2.tgz#4a3e974ec0cba9004d3fc6cde7209ea69368a621"
+
+isexe@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10"
+
+isobject@^2.0.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/isobject/-/isobject-2.1.0.tgz#f065561096a3f1da2ef46272f815c840d87e0c89"
+  dependencies:
+    isarray "1.0.0"
+
+isobject@^3.0.0, isobject@^3.0.1:
+  version "3.0.1"
+  resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df"
+
+isstream@~0.1.2:
+  version "0.1.2"
+  resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a"
+
+istanbul-lib-coverage@^1.2.0:
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/istanbul-lib-coverage/-/istanbul-lib-coverage-1.2.0.tgz#f7d8f2e42b97e37fe796114cb0f9d68b5e3a4341"
+
+istanbul-lib-instrument@^1.10.1:
+  version "1.10.1"
+  resolved "https://registry.yarnpkg.com/istanbul-lib-instrument/-/istanbul-lib-instrument-1.10.1.tgz#724b4b6caceba8692d3f1f9d0727e279c401af7b"
+  dependencies:
+    babel-generator "^6.18.0"
+    babel-template "^6.16.0"
+    babel-traverse "^6.18.0"
+    babel-types "^6.18.0"
+    babylon "^6.18.0"
+    istanbul-lib-coverage "^1.2.0"
+    semver "^5.3.0"
+
+istanbul@^0.4.0:
+  version "0.4.5"
+  resolved "https://registry.yarnpkg.com/istanbul/-/istanbul-0.4.5.tgz#65c7d73d4c4da84d4f3ac310b918fb0b8033733b"
+  dependencies:
+    abbrev "1.0.x"
+    async "1.x"
+    escodegen "1.8.x"
+    esprima "2.7.x"
+    glob "^5.0.15"
+    handlebars "^4.0.1"
+    js-yaml "3.x"
+    mkdirp "0.5.x"
+    nopt "3.x"
+    once "1.x"
+    resolve "1.1.x"
+    supports-color "^3.1.0"
+    which "^1.1.1"
+    wordwrap "^1.0.0"
+
+jquery@x.*:
+  version "3.3.1"
+  resolved "https://registry.yarnpkg.com/jquery/-/jquery-3.3.1.tgz#958ce29e81c9790f31be7792df5d4d95fc57fbca"
+
+js-base64@^2.1.8, js-base64@^2.1.9:
+  version "2.4.3"
+  resolved "https://registry.yarnpkg.com/js-base64/-/js-base64-2.4.3.tgz#2e545ec2b0f2957f41356510205214e98fad6582"
+
+js-beautify@^1.6.3:
+  version "1.7.5"
+  resolved "https://registry.yarnpkg.com/js-beautify/-/js-beautify-1.7.5.tgz#69d9651ef60dbb649f65527b53674950138a7919"
+  dependencies:
+    config-chain "~1.1.5"
+    editorconfig "^0.13.2"
+    mkdirp "~0.5.0"
+    nopt "~3.0.1"
+
+js-logger@^1.3.0:
+  version "1.4.1"
+  resolved "https://registry.yarnpkg.com/js-logger/-/js-logger-1.4.1.tgz#f0230dc5e84e120f213d6e5a6b767a913d290335"
+
+js-tokens@^3.0.0, js-tokens@^3.0.2:
+  version "3.0.2"
+  resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-3.0.2.tgz#9866df395102130e38f7f996bceb65443209c25b"
+
+js-yaml@3.x, js-yaml@^3.4.3, js-yaml@^3.5.1:
+  version "3.11.0"
+  resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.11.0.tgz#597c1a8bd57152f26d622ce4117851a51f5ebaef"
+  dependencies:
+    argparse "^1.0.7"
+    esprima "^4.0.0"
+
+js-yaml@~3.7.0:
+  version "3.7.0"
+  resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.7.0.tgz#5c967ddd837a9bfdca5f2de84253abe8a1c03b80"
+  dependencies:
+    argparse "^1.0.7"
+    esprima "^2.6.0"
+
+jsbn@~0.1.0:
+  version "0.1.1"
+  resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513"
+
+jsesc@^1.3.0:
+  version "1.3.0"
+  resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-1.3.0.tgz#46c3fec8c1892b12b0833db9bc7622176dbab34b"
+
+jsesc@~0.5.0:
+  version "0.5.0"
+  resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-0.5.0.tgz#e7dee66e35d6fc16f710fe91d5cf69f70f08911d"
+
+json-loader@^0.5.4:
+  version "0.5.7"
+  resolved "https://registry.yarnpkg.com/json-loader/-/json-loader-0.5.7.tgz#dca14a70235ff82f0ac9a3abeb60d337a365185d"
+
+json-schema-traverse@^0.3.0:
+  version "0.3.1"
+  resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz#349a6d44c53a51de89b40805c5d5e59b417d3340"
+
+json-schema@0.2.3:
+  version "0.2.3"
+  resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.2.3.tgz#b480c892e59a2f05954ce727bd3f2a4e882f9e13"
+
+json-stable-stringify@^1.0.0, json-stable-stringify@^1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/json-stable-stringify/-/json-stable-stringify-1.0.1.tgz#9a759d39c5f2ff503fd5300646ed445f88c4f9af"
+  dependencies:
+    jsonify "~0.0.0"
+
+json-stringify-safe@~5.0.1:
+  version "5.0.1"
+  resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb"
+
+json3@3.3.2:
+  version "3.3.2"
+  resolved "https://registry.yarnpkg.com/json3/-/json3-3.3.2.tgz#3c0434743df93e2f5c42aee7b19bcb483575f4e1"
+
+json5@^0.5.0, json5@^0.5.1:
+  version "0.5.1"
+  resolved "https://registry.yarnpkg.com/json5/-/json5-0.5.1.tgz#1eade7acc012034ad84e2396767ead9fa5495821"
+
+jsonfile@^2.1.0:
+  version "2.4.0"
+  resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-2.4.0.tgz#3736a2b428b87bbda0cc83b53fa3d633a35c2ae8"
+  optionalDependencies:
+    graceful-fs "^4.1.6"
+
+jsonify@~0.0.0:
+  version "0.0.0"
+  resolved "https://registry.yarnpkg.com/jsonify/-/jsonify-0.0.0.tgz#2c74b6ee41d93ca51b7b5aaee8f503631d252a73"
+
+jsonpointer@^4.0.0:
+  version "4.0.1"
+  resolved "https://registry.yarnpkg.com/jsonpointer/-/jsonpointer-4.0.1.tgz#4fd92cb34e0e9db3c89c8622ecf51f9b978c6cb9"
+
+jsprim@^1.2.2:
+  version "1.4.1"
+  resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.1.tgz#313e66bc1e5cc06e438bc1b7499c2e5c56acb6a2"
+  dependencies:
+    assert-plus "1.0.0"
+    extsprintf "1.3.0"
+    json-schema "0.2.3"
+    verror "1.10.0"
+
+jwt-decode@^2.2.0:
+  version "2.2.0"
+  resolved "https://registry.yarnpkg.com/jwt-decode/-/jwt-decode-2.2.0.tgz#7d86bd56679f58ce6a84704a657dd392bba81a79"
+
+karma-coverage@^1.1.1:
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/karma-coverage/-/karma-coverage-1.1.1.tgz#5aff8b39cf6994dc22de4c84362c76001b637cf6"
+  dependencies:
+    dateformat "^1.0.6"
+    istanbul "^0.4.0"
+    lodash "^3.8.0"
+    minimatch "^3.0.0"
+    source-map "^0.5.1"
+
+karma-mocha@^1.3.0:
+  version "1.3.0"
+  resolved "https://registry.yarnpkg.com/karma-mocha/-/karma-mocha-1.3.0.tgz#eeaac7ffc0e201eb63c467440d2b69c7cf3778bf"
+  dependencies:
+    minimist "1.2.0"
+
+karma-phantomjs-launcher@^1.0.2:
+  version "1.0.4"
+  resolved "https://registry.yarnpkg.com/karma-phantomjs-launcher/-/karma-phantomjs-launcher-1.0.4.tgz#d23ca34801bda9863ad318e3bb4bd4062b13acd2"
+  dependencies:
+    lodash "^4.0.1"
+    phantomjs-prebuilt "^2.1.7"
+
+karma-phantomjs-shim@^1.4.0:
+  version "1.5.0"
+  resolved "https://registry.yarnpkg.com/karma-phantomjs-shim/-/karma-phantomjs-shim-1.5.0.tgz#e8db65883480f0dbd184cc961d39c64511742200"
+
+karma-sinon-chai@^1.3.1:
+  version "1.3.3"
+  resolved "https://registry.yarnpkg.com/karma-sinon-chai/-/karma-sinon-chai-1.3.3.tgz#a597e5b4a1369fe7b3d7d76c09ed2061a38e747f"
+  dependencies:
+    lolex "^1.6.0"
+
+karma-sinon-stub-promise@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/karma-sinon-stub-promise/-/karma-sinon-stub-promise-1.0.0.tgz#617e9666e6fce77d63082cb94b637974c84a36bc"
+
+karma-sourcemap-loader@^0.3.7:
+  version "0.3.7"
+  resolved "https://registry.yarnpkg.com/karma-sourcemap-loader/-/karma-sourcemap-loader-0.3.7.tgz#91322c77f8f13d46fed062b042e1009d4c4505d8"
+  dependencies:
+    graceful-fs "^4.1.2"
+
+karma-spec-reporter@0.0.30:
+  version "0.0.30"
+  resolved "https://registry.yarnpkg.com/karma-spec-reporter/-/karma-spec-reporter-0.0.30.tgz#d10b5c8bb441cb1c6adf56785f89d395f2e9093a"
+  dependencies:
+    colors "^1.1.2"
+
+karma-webpack@^2.0.2:
+  version "2.0.13"
+  resolved "https://registry.yarnpkg.com/karma-webpack/-/karma-webpack-2.0.13.tgz#cf56e3056c15b7747a0bb2140fc9a6be41dd9f02"
+  dependencies:
+    async "^2.0.0"
+    babel-runtime "^6.0.0"
+    loader-utils "^1.0.0"
+    lodash "^4.0.0"
+    source-map "^0.5.6"
+    webpack-dev-middleware "^1.12.0"
+
+karma@^1.4.1:
+  version "1.7.1"
+  resolved "https://registry.yarnpkg.com/karma/-/karma-1.7.1.tgz#85cc08e9e0a22d7ce9cca37c4a1be824f6a2b1ae"
+  dependencies:
+    bluebird "^3.3.0"
+    body-parser "^1.16.1"
+    chokidar "^1.4.1"
+    colors "^1.1.0"
+    combine-lists "^1.0.0"
+    connect "^3.6.0"
+    core-js "^2.2.0"
+    di "^0.0.1"
+    dom-serialize "^2.2.0"
+    expand-braces "^0.1.1"
+    glob "^7.1.1"
+    graceful-fs "^4.1.2"
+    http-proxy "^1.13.0"
+    isbinaryfile "^3.0.0"
+    lodash "^3.8.0"
+    log4js "^0.6.31"
+    mime "^1.3.4"
+    minimatch "^3.0.2"
+    optimist "^0.6.1"
+    qjobs "^1.1.4"
+    range-parser "^1.2.0"
+    rimraf "^2.6.0"
+    safe-buffer "^5.0.1"
+    socket.io "1.7.3"
+    source-map "^0.5.3"
+    tmp "0.0.31"
+    useragent "^2.1.12"
+
+kew@^0.7.0:
+  version "0.7.0"
+  resolved "https://registry.yarnpkg.com/kew/-/kew-0.7.0.tgz#79d93d2d33363d6fdd2970b335d9141ad591d79b"
+
+kind-of@^3.0.2, kind-of@^3.0.3, kind-of@^3.2.0:
+  version "3.2.2"
+  resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-3.2.2.tgz#31ea21a734bab9bbb0f32466d893aea51e4a3c64"
+  dependencies:
+    is-buffer "^1.1.5"
+
+kind-of@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-4.0.0.tgz#20813df3d712928b207378691a45066fae72dd57"
+  dependencies:
+    is-buffer "^1.1.5"
+
+kind-of@^5.0.0:
+  version "5.1.0"
+  resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-5.1.0.tgz#729c91e2d857b7a419a1f9aa65685c4c33f5845d"
+
+kind-of@^6.0.0, kind-of@^6.0.2:
+  version "6.0.2"
+  resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.2.tgz#01146b36a6218e64e58f3a8d66de5d7fc6f6d051"
+
+klaw@^1.0.0:
+  version "1.3.1"
+  resolved "https://registry.yarnpkg.com/klaw/-/klaw-1.3.1.tgz#4088433b46b3b1ba259d78785d8e96f73ba02439"
+  optionalDependencies:
+    graceful-fs "^4.1.9"
+
+lazy-cache@^1.0.3:
+  version "1.0.4"
+  resolved "https://registry.yarnpkg.com/lazy-cache/-/lazy-cache-1.0.4.tgz#a1d78fc3a50474cb80845d3b3b6e1da49a446e8e"
+
+lcid@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/lcid/-/lcid-1.0.0.tgz#308accafa0bc483a3867b4b6f2b9506251d1b835"
+  dependencies:
+    invert-kv "^1.0.0"
+
+levn@^0.3.0, levn@~0.3.0:
+  version "0.3.0"
+  resolved "https://registry.yarnpkg.com/levn/-/levn-0.3.0.tgz#3b09924edf9f083c0490fdd4c0bc4421e04764ee"
+  dependencies:
+    prelude-ls "~1.1.2"
+    type-check "~0.3.2"
+
+load-json-file@^1.0.0:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-1.1.0.tgz#956905708d58b4bab4c2261b04f59f31c99374c0"
+  dependencies:
+    graceful-fs "^4.1.2"
+    parse-json "^2.2.0"
+    pify "^2.0.0"
+    pinkie-promise "^2.0.0"
+    strip-bom "^2.0.0"
+
+load-json-file@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-2.0.0.tgz#7947e42149af80d696cbf797bcaabcfe1fe29ca8"
+  dependencies:
+    graceful-fs "^4.1.2"
+    parse-json "^2.2.0"
+    pify "^2.0.0"
+    strip-bom "^3.0.0"
+
+loader-fs-cache@^1.0.0:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/loader-fs-cache/-/loader-fs-cache-1.0.1.tgz#56e0bf08bd9708b26a765b68509840c8dec9fdbc"
+  dependencies:
+    find-cache-dir "^0.1.1"
+    mkdirp "0.5.1"
+
+loader-runner@^2.3.0:
+  version "2.3.0"
+  resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-2.3.0.tgz#f482aea82d543e07921700d5a46ef26fdac6b8a2"
+
+loader-utils@^0.2.16:
+  version "0.2.17"
+  resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-0.2.17.tgz#f86e6374d43205a6e6c60e9196f17c0299bfb348"
+  dependencies:
+    big.js "^3.1.3"
+    emojis-list "^2.0.0"
+    json5 "^0.5.0"
+    object-assign "^4.0.1"
+
+loader-utils@^1.0.0, loader-utils@^1.0.1, loader-utils@^1.0.2, loader-utils@^1.1.0:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-1.1.0.tgz#c98aef488bcceda2ffb5e2de646d6a754429f5cd"
+  dependencies:
+    big.js "^3.1.3"
+    emojis-list "^2.0.0"
+    json5 "^0.5.0"
+
+locate-path@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-2.0.0.tgz#2b568b265eec944c6d9c0de9c3dbbbca0354cd8e"
+  dependencies:
+    p-locate "^2.0.0"
+    path-exists "^3.0.0"
+
+lodash._arraycopy@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/lodash._arraycopy/-/lodash._arraycopy-3.0.0.tgz#76e7b7c1f1fb92547374878a562ed06a3e50f6e1"
+
+lodash._arrayeach@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/lodash._arrayeach/-/lodash._arrayeach-3.0.0.tgz#bab156b2a90d3f1bbd5c653403349e5e5933ef9e"
+
+lodash._baseassign@^3.0.0:
+  version "3.2.0"
+  resolved "https://registry.yarnpkg.com/lodash._baseassign/-/lodash._baseassign-3.2.0.tgz#8c38a099500f215ad09e59f1722fd0c52bfe0a4e"
+  dependencies:
+    lodash._basecopy "^3.0.0"
+    lodash.keys "^3.0.0"
+
+lodash._baseclone@^3.0.0:
+  version "3.3.0"
+  resolved "https://registry.yarnpkg.com/lodash._baseclone/-/lodash._baseclone-3.3.0.tgz#303519bf6393fe7e42f34d8b630ef7794e3542b7"
+  dependencies:
+    lodash._arraycopy "^3.0.0"
+    lodash._arrayeach "^3.0.0"
+    lodash._baseassign "^3.0.0"
+    lodash._basefor "^3.0.0"
+    lodash.isarray "^3.0.0"
+    lodash.keys "^3.0.0"
+
+lodash._baseclone@^4.0.0:
+  version "4.5.7"
+  resolved "https://registry.yarnpkg.com/lodash._baseclone/-/lodash._baseclone-4.5.7.tgz#ce42ade08384ef5d62fa77c30f61a46e686f8434"
+
+lodash._basecopy@^3.0.0:
+  version "3.0.1"
+  resolved "https://registry.yarnpkg.com/lodash._basecopy/-/lodash._basecopy-3.0.1.tgz#8da0e6a876cf344c0ad8a54882111dd3c5c7ca36"
+
+lodash._basecreate@^3.0.0:
+  version "3.0.3"
+  resolved "https://registry.yarnpkg.com/lodash._basecreate/-/lodash._basecreate-3.0.3.tgz#1bc661614daa7fc311b7d03bf16806a0213cf821"
+
+lodash._basefor@^3.0.0:
+  version "3.0.3"
+  resolved "https://registry.yarnpkg.com/lodash._basefor/-/lodash._basefor-3.0.3.tgz#7550b4e9218ef09fad24343b612021c79b4c20c2"
+
+lodash._bindcallback@^3.0.0:
+  version "3.0.1"
+  resolved "https://registry.yarnpkg.com/lodash._bindcallback/-/lodash._bindcallback-3.0.1.tgz#e531c27644cf8b57a99e17ed95b35c748789392e"
+
+lodash._getnative@^3.0.0:
+  version "3.9.1"
+  resolved "https://registry.yarnpkg.com/lodash._getnative/-/lodash._getnative-3.9.1.tgz#570bc7dede46d61cdcde687d65d3eecbaa3aaff5"
+
+lodash._isiterateecall@^3.0.0:
+  version "3.0.9"
+  resolved "https://registry.yarnpkg.com/lodash._isiterateecall/-/lodash._isiterateecall-3.0.9.tgz#5203ad7ba425fae842460e696db9cf3e6aac057c"
+
+lodash._stack@^4.0.0:
+  version "4.1.3"
+  resolved "https://registry.yarnpkg.com/lodash._stack/-/lodash._stack-4.1.3.tgz#751aa76c1b964b047e76d14fc72a093fcb5e2dd0"
+
+lodash.assign@^4.2.0:
+  version "4.2.0"
+  resolved "https://registry.yarnpkg.com/lodash.assign/-/lodash.assign-4.2.0.tgz#0d99f3ccd7a6d261d19bdaeb9245005d285808e7"
+
+lodash.assignin@^4.0.9:
+  version "4.2.0"
+  resolved "https://registry.yarnpkg.com/lodash.assignin/-/lodash.assignin-4.2.0.tgz#ba8df5fb841eb0a3e8044232b0e263a8dc6a28a2"
+
+lodash.bind@^4.1.4:
+  version "4.2.1"
+  resolved "https://registry.yarnpkg.com/lodash.bind/-/lodash.bind-4.2.1.tgz#7ae3017e939622ac31b7d7d7dcb1b34db1690d35"
+
+lodash.camelcase@^4.3.0:
+  version "4.3.0"
+  resolved "https://registry.yarnpkg.com/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz#b28aa6288a2b9fc651035c7711f65ab6190331a6"
+
+lodash.clone@3.0.3:
+  version "3.0.3"
+  resolved "https://registry.yarnpkg.com/lodash.clone/-/lodash.clone-3.0.3.tgz#84688c73d32b5a90ca25616963f189252a997043"
+  dependencies:
+    lodash._baseclone "^3.0.0"
+    lodash._bindcallback "^3.0.0"
+    lodash._isiterateecall "^3.0.0"
+
+lodash.clonedeep@^4.3.2:
+  version "4.5.0"
+  resolved "https://registry.yarnpkg.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz#e23f3f9c4f8fbdde872529c1071857a086e5ccef"
+
+lodash.create@3.1.1:
+  version "3.1.1"
+  resolved "https://registry.yarnpkg.com/lodash.create/-/lodash.create-3.1.1.tgz#d7f2849f0dbda7e04682bb8cd72ab022461debe7"
+  dependencies:
+    lodash._baseassign "^3.0.0"
+    lodash._basecreate "^3.0.0"
+    lodash._isiterateecall "^3.0.0"
+
+lodash.defaults@^4.0.1:
+  version "4.2.0"
+  resolved "https://registry.yarnpkg.com/lodash.defaults/-/lodash.defaults-4.2.0.tgz#d09178716ffea4dde9e5fb7b37f6f0802274580c"
+
+lodash.defaultsdeep@4.3.2:
+  version "4.3.2"
+  resolved "https://registry.yarnpkg.com/lodash.defaultsdeep/-/lodash.defaultsdeep-4.3.2.tgz#6c1a586e6c5647b0e64e2d798141b8836158be8a"
+  dependencies:
+    lodash._baseclone "^4.0.0"
+    lodash._stack "^4.0.0"
+    lodash.isplainobject "^4.0.0"
+    lodash.keysin "^4.0.0"
+    lodash.mergewith "^4.0.0"
+    lodash.rest "^4.0.0"
+
+lodash.filter@^4.4.0:
+  version "4.6.0"
+  resolved "https://registry.yarnpkg.com/lodash.filter/-/lodash.filter-4.6.0.tgz#668b1d4981603ae1cc5a6fa760143e480b4c4ace"
+
+lodash.flatten@^4.2.0:
+  version "4.4.0"
+  resolved "https://registry.yarnpkg.com/lodash.flatten/-/lodash.flatten-4.4.0.tgz#f31c22225a9632d2bbf8e4addbef240aa765a61f"
+
+lodash.foreach@^4.3.0:
+  version "4.5.0"
+  resolved "https://registry.yarnpkg.com/lodash.foreach/-/lodash.foreach-4.5.0.tgz#1a6a35eace401280c7f06dddec35165ab27e3e53"
+
+lodash.get@^4.4.2:
+  version "4.4.2"
+  resolved "https://registry.yarnpkg.com/lodash.get/-/lodash.get-4.4.2.tgz#2d177f652fa31e939b4438d5341499dfa3825e99"
+
+lodash.isarguments@^3.0.0:
+  version "3.1.0"
+  resolved "https://registry.yarnpkg.com/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz#2f573d85c6a24289ff00663b491c1d338ff3458a"
+
+lodash.isarray@^3.0.0:
+  version "3.0.4"
+  resolved "https://registry.yarnpkg.com/lodash.isarray/-/lodash.isarray-3.0.4.tgz#79e4eb88c36a8122af86f844aa9bcd851b5fbb55"
+
+lodash.isplainobject@^4.0.0:
+  version "4.0.6"
+  resolved "https://registry.yarnpkg.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz#7c526a52d89b45c45cc690b88163be0497f550cb"
+
+lodash.keys@^3.0.0:
+  version "3.1.2"
+  resolved "https://registry.yarnpkg.com/lodash.keys/-/lodash.keys-3.1.2.tgz#4dbc0472b156be50a0b286855d1bd0b0c656098a"
+  dependencies:
+    lodash._getnative "^3.0.0"
+    lodash.isarguments "^3.0.0"
+    lodash.isarray "^3.0.0"
+
+lodash.keysin@^4.0.0:
+  version "4.2.0"
+  resolved "https://registry.yarnpkg.com/lodash.keysin/-/lodash.keysin-4.2.0.tgz#8cc3fb35c2d94acc443a1863e02fa40799ea6f28"
+
+lodash.map@^4.4.0:
+  version "4.6.0"
+  resolved "https://registry.yarnpkg.com/lodash.map/-/lodash.map-4.6.0.tgz#771ec7839e3473d9c4cde28b19394c3562f4f6d3"
+
+lodash.memoize@^4.1.2:
+  version "4.1.2"
+  resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe"
+
+lodash.merge@^4.4.0:
+  version "4.6.1"
+  resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.1.tgz#adc25d9cb99b9391c59624f379fbba60d7111d54"
+
+lodash.mergewith@^4.0.0, lodash.mergewith@^4.6.0:
+  version "4.6.1"
+  resolved "https://registry.yarnpkg.com/lodash.mergewith/-/lodash.mergewith-4.6.1.tgz#639057e726c3afbdb3e7d42741caa8d6e4335927"
+
+lodash.pick@^4.2.1:
+  version "4.4.0"
+  resolved "https://registry.yarnpkg.com/lodash.pick/-/lodash.pick-4.4.0.tgz#52f05610fff9ded422611441ed1fc123a03001b3"
+
+lodash.reduce@^4.4.0:
+  version "4.6.0"
+  resolved "https://registry.yarnpkg.com/lodash.reduce/-/lodash.reduce-4.6.0.tgz#f1ab6b839299ad48f784abbf476596f03b914d3b"
+
+lodash.reject@^4.4.0:
+  version "4.6.0"
+  resolved "https://registry.yarnpkg.com/lodash.reject/-/lodash.reject-4.6.0.tgz#80d6492dc1470864bbf583533b651f42a9f52415"
+
+lodash.rest@^4.0.0:
+  version "4.0.5"
+  resolved "https://registry.yarnpkg.com/lodash.rest/-/lodash.rest-4.0.5.tgz#954ef75049262038c96d1fc98b28fdaf9f0772aa"
+
+lodash.some@^4.4.0:
+  version "4.6.0"
+  resolved "https://registry.yarnpkg.com/lodash.some/-/lodash.some-4.6.0.tgz#1bb9f314ef6b8baded13b549169b2a945eb68e4d"
+
+lodash.tail@^4.1.1:
+  version "4.1.1"
+  resolved "https://registry.yarnpkg.com/lodash.tail/-/lodash.tail-4.1.1.tgz#d2333a36d9e7717c8ad2f7cacafec7c32b444664"
+
+lodash.uniq@^4.5.0:
+  version "4.5.0"
+  resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773"
+
+lodash@^3.8.0:
+  version "3.10.1"
+  resolved "https://registry.yarnpkg.com/lodash/-/lodash-3.10.1.tgz#5bf45e8e49ba4189e17d482789dfd15bd140b7b6"
+
+lodash@^4.0.0, lodash@^4.0.1, lodash@^4.14.0, lodash@^4.17.2, lodash@^4.17.3, lodash@^4.17.4, lodash@^4.17.5, lodash@^4.3.0, lodash@^4.5.0, lodash@~4.17.4:
+  version "4.17.5"
+  resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.5.tgz#99a92d65c0272debe8c96b6057bc8fbfa3bed511"
+
+log-symbols@^2.1.0:
+  version "2.2.0"
+  resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-2.2.0.tgz#5740e1c5d6f0dfda4ad9323b5332107ef6b4c40a"
+  dependencies:
+    chalk "^2.0.1"
+
+log4js@^0.6.31:
+  version "0.6.38"
+  resolved "https://registry.yarnpkg.com/log4js/-/log4js-0.6.38.tgz#2c494116695d6fb25480943d3fc872e662a522fd"
+  dependencies:
+    readable-stream "~1.0.2"
+    semver "~4.3.3"
+
+lolex@^1.5.2, lolex@^1.6.0:
+  version "1.6.0"
+  resolved "https://registry.yarnpkg.com/lolex/-/lolex-1.6.0.tgz#3a9a0283452a47d7439e72731b9e07d7386e49f6"
+
+longest@^1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/longest/-/longest-1.0.1.tgz#30a0b2da38f73770e8294a0d22e6625ed77d0097"
+
+loose-envify@^1.0.0:
+  version "1.3.1"
+  resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.3.1.tgz#d1a8ad33fa9ce0e713d65fdd0ac8b748d478c848"
+  dependencies:
+    js-tokens "^3.0.0"
+
+loud-rejection@^1.0.0:
+  version "1.6.0"
+  resolved "https://registry.yarnpkg.com/loud-rejection/-/loud-rejection-1.6.0.tgz#5b46f80147edee578870f086d04821cf998e551f"
+  dependencies:
+    currently-unhandled "^0.4.1"
+    signal-exit "^3.0.0"
+
+lower-case@^1.1.1:
+  version "1.1.4"
+  resolved "https://registry.yarnpkg.com/lower-case/-/lower-case-1.1.4.tgz#9a2cabd1b9e8e0ae993a4bf7d5875c39c42e8eac"
+
+lru-cache@4.1.x, lru-cache@^4.0.1, lru-cache@^4.1.1:
+  version "4.1.2"
+  resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.1.2.tgz#45234b2e6e2f2b33da125624c4664929a0224c3f"
+  dependencies:
+    pseudomap "^1.0.2"
+    yallist "^2.1.2"
+
+lru-cache@^3.2.0:
+  version "3.2.0"
+  resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-3.2.0.tgz#71789b3b7f5399bec8565dda38aa30d2a097efee"
+  dependencies:
+    pseudomap "^1.0.1"
+
+lru-cache@~2.6.5:
+  version "2.6.5"
+  resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-2.6.5.tgz#e56d6354148ede8d7707b58d143220fd08df0fd5"
+
+macaddress@^0.2.8:
+  version "0.2.8"
+  resolved "https://registry.yarnpkg.com/macaddress/-/macaddress-0.2.8.tgz#5904dc537c39ec6dbefeae902327135fa8511f12"
+
+make-dir@^1.0.0:
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-1.2.0.tgz#6d6a49eead4aae296c53bbf3a1a008bd6c89469b"
+  dependencies:
+    pify "^3.0.0"
+
+map-cache@^0.2.2:
+  version "0.2.2"
+  resolved "https://registry.yarnpkg.com/map-cache/-/map-cache-0.2.2.tgz#c32abd0bd6525d9b051645bb4f26ac5dc98a0dbf"
+
+map-obj@^1.0.0, map-obj@^1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/map-obj/-/map-obj-1.0.1.tgz#d933ceb9205d82bdcf4886f6742bdc2b4dea146d"
+
+map-visit@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/map-visit/-/map-visit-1.0.0.tgz#ecdca8f13144e660f1b5bd41f12f3479d98dfb8f"
+  dependencies:
+    object-visit "^1.0.0"
+
+masonry-layout@4.1.1:
+  version "4.1.1"
+  resolved "https://registry.yarnpkg.com/masonry-layout/-/masonry-layout-4.1.1.tgz#e8c8c6f5a9e621a75203ac4b7000855a36f6753e"
+  dependencies:
+    get-size "^2.0.2"
+    outlayer "^2.1.0"
+
+masonry-layout@^4.2.1:
+  version "4.2.1"
+  resolved "https://registry.yarnpkg.com/masonry-layout/-/masonry-layout-4.2.1.tgz#1c878b85a88866ba6b39b9bd0e162348aed7ce13"
+  dependencies:
+    get-size "^2.0.2"
+    outlayer "^2.1.0"
+
+masonry@latest:
+  version "0.0.2"
+  resolved "https://registry.yarnpkg.com/masonry/-/masonry-0.0.2.tgz#c06a0b2efc043727eadd16b35fdae93a3908f432"
+  dependencies:
+    ejs "0.7.1"
+
+math-expression-evaluator@^1.2.14:
+  version "1.2.17"
+  resolved "https://registry.yarnpkg.com/math-expression-evaluator/-/math-expression-evaluator-1.2.17.tgz#de819fdbcd84dccd8fae59c6aeb79615b9d266ac"
+
+md5.js@^1.3.4:
+  version "1.3.4"
+  resolved "https://registry.yarnpkg.com/md5.js/-/md5.js-1.3.4.tgz#e9bdbde94a20a5ac18b04340fc5764d5b09d901d"
+  dependencies:
+    hash-base "^3.0.0"
+    inherits "^2.0.1"
+
+media-typer@0.3.0:
+  version "0.3.0"
+  resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748"
+
+mem@^1.1.0:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/mem/-/mem-1.1.0.tgz#5edd52b485ca1d900fe64895505399a0dfa45f76"
+  dependencies:
+    mimic-fn "^1.0.0"
+
+memory-fs@^0.4.0, memory-fs@~0.4.1:
+  version "0.4.1"
+  resolved "https://registry.yarnpkg.com/memory-fs/-/memory-fs-0.4.1.tgz#3a9a20b8462523e447cfbc7e8bb80ed667bfc552"
+  dependencies:
+    errno "^0.1.3"
+    readable-stream "^2.0.1"
+
+meow@^3.3.0, meow@^3.7.0:
+  version "3.7.0"
+  resolved "https://registry.yarnpkg.com/meow/-/meow-3.7.0.tgz#72cb668b425228290abbfa856892587308a801fb"
+  dependencies:
+    camelcase-keys "^2.0.0"
+    decamelize "^1.1.2"
+    loud-rejection "^1.0.0"
+    map-obj "^1.0.1"
+    minimist "^1.1.3"
+    normalize-package-data "^2.3.4"
+    object-assign "^4.0.1"
+    read-pkg-up "^1.0.1"
+    redent "^1.0.0"
+    trim-newlines "^1.0.0"
+
+merge-descriptors@1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61"
+
+methods@~1.1.2:
+  version "1.1.2"
+  resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee"
+
+micromatch@^2.1.5, micromatch@^2.3.11:
+  version "2.3.11"
+  resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-2.3.11.tgz#86677c97d1720b363431d04d0d15293bd38c1565"
+  dependencies:
+    arr-diff "^2.0.0"
+    array-unique "^0.2.1"
+    braces "^1.8.2"
+    expand-brackets "^0.1.4"
+    extglob "^0.3.1"
+    filename-regex "^2.0.0"
+    is-extglob "^1.0.0"
+    is-glob "^2.0.1"
+    kind-of "^3.0.2"
+    normalize-path "^2.0.1"
+    object.omit "^2.0.0"
+    parse-glob "^3.0.4"
+    regex-cache "^0.4.2"
+
+micromatch@^3.1.4, micromatch@^3.1.8:
+  version "3.1.10"
+  resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-3.1.10.tgz#70859bc95c9840952f359a068a3fc49f9ecfac23"
+  dependencies:
+    arr-diff "^4.0.0"
+    array-unique "^0.3.2"
+    braces "^2.3.1"
+    define-property "^2.0.2"
+    extend-shallow "^3.0.2"
+    extglob "^2.0.4"
+    fragment-cache "^0.2.1"
+    kind-of "^6.0.2"
+    nanomatch "^1.2.9"
+    object.pick "^1.3.0"
+    regex-not "^1.0.0"
+    snapdragon "^0.8.1"
+    to-regex "^3.0.2"
+
+miller-rabin@^4.0.0:
+  version "4.0.1"
+  resolved "https://registry.yarnpkg.com/miller-rabin/-/miller-rabin-4.0.1.tgz#f080351c865b0dc562a8462966daa53543c78a4d"
+  dependencies:
+    bn.js "^4.0.0"
+    brorand "^1.0.1"
+
+mime-db@~1.33.0:
+  version "1.33.0"
+  resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.33.0.tgz#a3492050a5cb9b63450541e39d9788d2272783db"
+
+mime-types@^2.1.12, mime-types@~2.1.11, mime-types@~2.1.17, mime-types@~2.1.18, mime-types@~2.1.7:
+  version "2.1.18"
+  resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.18.tgz#6f323f60a83d11146f831ff11fd66e2fe5503bb8"
+  dependencies:
+    mime-db "~1.33.0"
+
+mime@1.3.x:
+  version "1.3.6"
+  resolved "https://registry.yarnpkg.com/mime/-/mime-1.3.6.tgz#591d84d3653a6b0b4a3b9df8de5aa8108e72e5e0"
+
+mime@1.4.1:
+  version "1.4.1"
+  resolved "https://registry.yarnpkg.com/mime/-/mime-1.4.1.tgz#121f9ebc49e3766f311a76e1fa1c8003c4b03aa6"
+
+mime@^1.3.4, mime@^1.5.0:
+  version "1.6.0"
+  resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1"
+
+mimic-fn@^1.0.0:
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-1.2.0.tgz#820c86a39334640e99516928bd03fca88057d022"
+
+minimalistic-assert@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.0.tgz#702be2dda6b37f4836bcb3f5db56641b64a1d3d3"
+
+minimalistic-crypto-utils@^1.0.0, minimalistic-crypto-utils@^1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz#f6c00c1c0b082246e5c4d99dfb8c7c083b2b582a"
+
+"minimatch@2 || 3", minimatch@^3.0.0, minimatch@^3.0.2, minimatch@^3.0.4, minimatch@~3.0.2:
+  version "3.0.4"
+  resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083"
+  dependencies:
+    brace-expansion "^1.1.7"
+
+minimatch@3.0.3:
+  version "3.0.3"
+  resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.3.tgz#2a4e4090b96b2db06a9d7df01055a62a77c9b774"
+  dependencies:
+    brace-expansion "^1.0.0"
+
+minimist@0.0.8:
+  version "0.0.8"
+  resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d"
+
+minimist@1.2.0, minimist@^1.1.3, minimist@^1.2.0:
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284"
+
+minimist@~0.0.1:
+  version "0.0.10"
+  resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.10.tgz#de3f98543dbf96082be48ad1a0c7cda836301dcf"
+
+mississippi@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/mississippi/-/mississippi-2.0.0.tgz#3442a508fafc28500486feea99409676e4ee5a6f"
+  dependencies:
+    concat-stream "^1.5.0"
+    duplexify "^3.4.2"
+    end-of-stream "^1.1.0"
+    flush-write-stream "^1.0.0"
+    from2 "^2.1.0"
+    parallel-transform "^1.1.0"
+    pump "^2.0.1"
+    pumpify "^1.3.3"
+    stream-each "^1.1.0"
+    through2 "^2.0.0"
+
+mixin-deep@^1.2.0:
+  version "1.3.1"
+  resolved "https://registry.yarnpkg.com/mixin-deep/-/mixin-deep-1.3.1.tgz#a49e7268dce1a0d9698e45326c5626df3543d0fe"
+  dependencies:
+    for-in "^1.0.2"
+    is-extendable "^1.0.1"
+
+mixin-object@^2.0.1:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/mixin-object/-/mixin-object-2.0.1.tgz#4fb949441dab182540f1fe035ba60e1947a5e57e"
+  dependencies:
+    for-in "^0.1.3"
+    is-extendable "^0.1.1"
+
+mkdirp@0.5.0:
+  version "0.5.0"
+  resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.0.tgz#1d73076a6df986cd9344e15e71fcc05a4c9abf12"
+  dependencies:
+    minimist "0.0.8"
+
+mkdirp@0.5.1, mkdirp@0.5.x, "mkdirp@>=0.5 0", mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp@~0.5.0, mkdirp@~0.5.1:
+  version "0.5.1"
+  resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903"
+  dependencies:
+    minimist "0.0.8"
+
+mkpath@1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/mkpath/-/mkpath-1.0.0.tgz#ebb3a977e7af1c683ae6fda12b545a6ba6c5853d"
+
+mocha-nightwatch@3.2.2:
+  version "3.2.2"
+  resolved "https://registry.yarnpkg.com/mocha-nightwatch/-/mocha-nightwatch-3.2.2.tgz#91bcb9b3bde057dd7677c78125e491e58d66647c"
+  dependencies:
+    browser-stdout "1.3.0"
+    commander "2.9.0"
+    debug "2.2.0"
+    diff "1.4.0"
+    escape-string-regexp "1.0.5"
+    glob "7.0.5"
+    growl "1.9.2"
+    json3 "3.3.2"
+    lodash.create "3.1.1"
+    mkdirp "0.5.1"
+    supports-color "3.1.2"
+
+mocha@^3.2.0:
+  version "3.5.3"
+  resolved "https://registry.yarnpkg.com/mocha/-/mocha-3.5.3.tgz#1e0480fe36d2da5858d1eb6acc38418b26eaa20d"
+  dependencies:
+    browser-stdout "1.3.0"
+    commander "2.9.0"
+    debug "2.6.8"
+    diff "3.2.0"
+    escape-string-regexp "1.0.5"
+    glob "7.1.1"
+    growl "1.9.2"
+    he "1.1.1"
+    json3 "3.3.2"
+    lodash.create "3.1.1"
+    mkdirp "0.5.1"
+    supports-color "3.1.2"
+
+moment@^2.20.1:
+  version "2.22.0"
+  resolved "https://registry.yarnpkg.com/moment/-/moment-2.22.0.tgz#7921ade01017dd45186e7fee5f424f0b8663a730"
+
+move-concurrently@^1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/move-concurrently/-/move-concurrently-1.0.1.tgz#be2c005fda32e0b29af1f05d7c4b33214c701f92"
+  dependencies:
+    aproba "^1.1.1"
+    copy-concurrently "^1.0.0"
+    fs-write-stream-atomic "^1.0.8"
+    mkdirp "^0.5.1"
+    rimraf "^2.5.4"
+    run-queue "^1.0.3"
+
+moxios@^0.4.0:
+  version "0.4.0"
+  resolved "https://registry.yarnpkg.com/moxios/-/moxios-0.4.0.tgz#fc0da2c65477d725ca6b9679d58370ed0c52f53b"
+
+ms@0.7.1:
+  version "0.7.1"
+  resolved "https://registry.yarnpkg.com/ms/-/ms-0.7.1.tgz#9cd13c03adbff25b65effde7ce864ee952017098"
+
+ms@0.7.2:
+  version "0.7.2"
+  resolved "https://registry.yarnpkg.com/ms/-/ms-0.7.2.tgz#ae25cf2512b3885a1d95d7f037868d8431124765"
+
+ms@2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
+
+mute-stream@0.0.5:
+  version "0.0.5"
+  resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.5.tgz#8fbfabb0a98a253d3184331f9e8deb7372fac6c0"
+
+nan@^2.10.0, nan@^2.3.0:
+  version "2.10.0"
+  resolved "https://registry.yarnpkg.com/nan/-/nan-2.10.0.tgz#96d0cd610ebd58d4b4de9cc0c6828cda99c7548f"
+
+nanomatch@^1.2.9:
+  version "1.2.9"
+  resolved "https://registry.yarnpkg.com/nanomatch/-/nanomatch-1.2.9.tgz#879f7150cb2dab7a471259066c104eee6e0fa7c2"
+  dependencies:
+    arr-diff "^4.0.0"
+    array-unique "^0.3.2"
+    define-property "^2.0.2"
+    extend-shallow "^3.0.2"
+    fragment-cache "^0.2.1"
+    is-odd "^2.0.0"
+    is-windows "^1.0.2"
+    kind-of "^6.0.2"
+    object.pick "^1.3.0"
+    regex-not "^1.0.0"
+    snapdragon "^0.8.1"
+    to-regex "^3.0.1"
+
+native-promise-only@^0.8.1:
+  version "0.8.1"
+  resolved "https://registry.yarnpkg.com/native-promise-only/-/native-promise-only-0.8.1.tgz#20a318c30cb45f71fe7adfbf7b21c99c1472ef11"
+
+natural-compare@^1.4.0:
+  version "1.4.0"
+  resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7"
+
+negotiator@0.6.1:
+  version "0.6.1"
+  resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.1.tgz#2b327184e8992101177b28563fb5e7102acd0ca9"
+
+neo-async@^2.5.0:
+  version "2.5.0"
+  resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.5.0.tgz#76b1c823130cca26acfbaccc8fbaf0a2fa33b18f"
+
+netmask@~1.0.4:
+  version "1.0.6"
+  resolved "https://registry.yarnpkg.com/netmask/-/netmask-1.0.6.tgz#20297e89d86f6f6400f250d9f4f6b4c1945fcd35"
+
+next-tick@1:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/next-tick/-/next-tick-1.0.0.tgz#ca86d1fe8828169b0120208e3dc8424b9db8342c"
+
+nightwatch@^0.9.12:
+  version "0.9.20"
+  resolved "https://registry.yarnpkg.com/nightwatch/-/nightwatch-0.9.20.tgz#156d17cd058cbc31f43ab18e915f7ec297fb53e0"
+  dependencies:
+    chai-nightwatch "~0.1.x"
+    ejs "2.5.7"
+    lodash.clone "3.0.3"
+    lodash.defaultsdeep "4.3.2"
+    minimatch "3.0.3"
+    mkpath "1.0.0"
+    mocha-nightwatch "3.2.2"
+    optimist "0.6.1"
+    proxy-agent "2.0.0"
+    q "1.4.1"
+
+no-case@^2.2.0:
+  version "2.3.2"
+  resolved "https://registry.yarnpkg.com/no-case/-/no-case-2.3.2.tgz#60b813396be39b3f1288a4c1ed5d1e7d28b464ac"
+  dependencies:
+    lower-case "^1.1.1"
+
+node-gettext@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/node-gettext/-/node-gettext-2.0.0.tgz#f1dc1237cdc546f51593da340304b8beba5b8525"
+  dependencies:
+    lodash.get "^4.4.2"
+
+node-gyp@^3.3.1:
+  version "3.6.2"
+  resolved "https://registry.yarnpkg.com/node-gyp/-/node-gyp-3.6.2.tgz#9bfbe54562286284838e750eac05295853fa1c60"
+  dependencies:
+    fstream "^1.0.0"
+    glob "^7.0.3"
+    graceful-fs "^4.1.2"
+    minimatch "^3.0.2"
+    mkdirp "^0.5.0"
+    nopt "2 || 3"
+    npmlog "0 || 1 || 2 || 3 || 4"
+    osenv "0"
+    request "2"
+    rimraf "2"
+    semver "~5.3.0"
+    tar "^2.0.0"
+    which "1"
+
+node-libs-browser@^2.0.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/node-libs-browser/-/node-libs-browser-2.1.0.tgz#5f94263d404f6e44767d726901fff05478d600df"
+  dependencies:
+    assert "^1.1.1"
+    browserify-zlib "^0.2.0"
+    buffer "^4.3.0"
+    console-browserify "^1.1.0"
+    constants-browserify "^1.0.0"
+    crypto-browserify "^3.11.0"
+    domain-browser "^1.1.1"
+    events "^1.0.0"
+    https-browserify "^1.0.0"
+    os-browserify "^0.3.0"
+    path-browserify "0.0.0"
+    process "^0.11.10"
+    punycode "^1.2.4"
+    querystring-es3 "^0.2.0"
+    readable-stream "^2.3.3"
+    stream-browserify "^2.0.1"
+    stream-http "^2.7.2"
+    string_decoder "^1.0.0"
+    timers-browserify "^2.0.4"
+    tty-browserify "0.0.0"
+    url "^0.11.0"
+    util "^0.10.3"
+    vm-browserify "0.0.4"
+
+node-pre-gyp@^0.6.39:
+  version "0.6.39"
+  resolved "https://registry.yarnpkg.com/node-pre-gyp/-/node-pre-gyp-0.6.39.tgz#c00e96860b23c0e1420ac7befc5044e1d78d8649"
+  dependencies:
+    detect-libc "^1.0.2"
+    hawk "3.1.3"
+    mkdirp "^0.5.1"
+    nopt "^4.0.1"
+    npmlog "^4.0.2"
+    rc "^1.1.7"
+    request "2.81.0"
+    rimraf "^2.6.1"
+    semver "^5.3.0"
+    tar "^2.2.1"
+    tar-pack "^3.4.0"
+
+node-sass@^4.5.3:
+  version "4.8.3"
+  resolved "https://registry.yarnpkg.com/node-sass/-/node-sass-4.8.3.tgz#d077cc20a08ac06f661ca44fb6f19cd2ed41debb"
+  dependencies:
+    async-foreach "^0.1.3"
+    chalk "^1.1.1"
+    cross-spawn "^3.0.0"
+    gaze "^1.0.0"
+    get-stdin "^4.0.1"
+    glob "^7.0.3"
+    in-publish "^2.0.0"
+    lodash.assign "^4.2.0"
+    lodash.clonedeep "^4.3.2"
+    lodash.mergewith "^4.6.0"
+    meow "^3.7.0"
+    mkdirp "^0.5.1"
+    nan "^2.10.0"
+    node-gyp "^3.3.1"
+    npmlog "^4.0.0"
+    request "~2.79.0"
+    sass-graph "^2.2.4"
+    stdout-stream "^1.4.0"
+    "true-case-path" "^1.0.2"
+
+"nopt@2 || 3", nopt@3.x, nopt@~3.0.1:
+  version "3.0.6"
+  resolved "https://registry.yarnpkg.com/nopt/-/nopt-3.0.6.tgz#c6465dbf08abcd4db359317f79ac68a646b28ff9"
+  dependencies:
+    abbrev "1"
+
+nopt@^4.0.1:
+  version "4.0.1"
+  resolved "https://registry.yarnpkg.com/nopt/-/nopt-4.0.1.tgz#d0d4685afd5415193c8c7505602d0d17cd64474d"
+  dependencies:
+    abbrev "1"
+    osenv "^0.1.4"
+
+normalize-package-data@^2.3.2, normalize-package-data@^2.3.4:
+  version "2.4.0"
+  resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.4.0.tgz#12f95a307d58352075a04907b84ac8be98ac012f"
+  dependencies:
+    hosted-git-info "^2.1.4"
+    is-builtin-module "^1.0.0"
+    semver "2 || 3 || 4 || 5"
+    validate-npm-package-license "^3.0.1"
+
+normalize-path@^2.0.0, normalize-path@^2.0.1, normalize-path@^2.1.1:
+  version "2.1.1"
+  resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-2.1.1.tgz#1ab28b556e198363a8c1a6f7e6fa20137fe6aed9"
+  dependencies:
+    remove-trailing-separator "^1.0.1"
+
+normalize-range@^0.1.2:
+  version "0.1.2"
+  resolved "https://registry.yarnpkg.com/normalize-range/-/normalize-range-0.1.2.tgz#2d10c06bdfd312ea9777695a4d28439456b75942"
+
+normalize-url@^1.4.0:
+  version "1.9.1"
+  resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-1.9.1.tgz#2cc0d66b31ea23036458436e3620d85954c66c3c"
+  dependencies:
+    object-assign "^4.0.1"
+    prepend-http "^1.0.0"
+    query-string "^4.1.0"
+    sort-keys "^1.0.0"
+
+npm-run-path@^2.0.0:
+  version "2.0.2"
+  resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-2.0.2.tgz#35a9232dfa35d7067b4cb2ddf2357b1871536c5f"
+  dependencies:
+    path-key "^2.0.0"
+
+"npmlog@0 || 1 || 2 || 3 || 4", npmlog@^4.0.0, npmlog@^4.0.2:
+  version "4.1.2"
+  resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-4.1.2.tgz#08a7f2a8bf734604779a9efa4ad5cc717abb954b"
+  dependencies:
+    are-we-there-yet "~1.1.2"
+    console-control-strings "~1.1.0"
+    gauge "~2.7.3"
+    set-blocking "~2.0.0"
+
+nth-check@~1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-1.0.1.tgz#9929acdf628fc2c41098deab82ac580cf149aae4"
+  dependencies:
+    boolbase "~1.0.0"
+
+num2fraction@^1.2.2:
+  version "1.2.2"
+  resolved "https://registry.yarnpkg.com/num2fraction/-/num2fraction-1.2.2.tgz#6f682b6a027a4e9ddfa4564cd2589d1d4e669ede"
+
+number-is-nan@^1.0.0:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/number-is-nan/-/number-is-nan-1.0.1.tgz#097b602b53422a522c1afb8790318336941a011d"
+
+oauth-sign@~0.8.1, oauth-sign@~0.8.2:
+  version "0.8.2"
+  resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.8.2.tgz#46a6ab7f0aead8deae9ec0565780b7d4efeb9d43"
+
+object-assign@4.1.0:
+  version "4.1.0"
+  resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.0.tgz#7a3b3d0e98063d43f4c03f2e8ae6cd51a86883a0"
+
+object-assign@^4.0.1, object-assign@^4.1.0, object-assign@^4.1.1:
+  version "4.1.1"
+  resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863"
+
+object-component@0.0.3:
+  version "0.0.3"
+  resolved "https://registry.yarnpkg.com/object-component/-/object-component-0.0.3.tgz#f0c69aa50efc95b866c186f400a33769cb2f1291"
+
+object-copy@^0.1.0:
+  version "0.1.0"
+  resolved "https://registry.yarnpkg.com/object-copy/-/object-copy-0.1.0.tgz#7e7d858b781bd7c991a41ba975ed3812754e998c"
+  dependencies:
+    copy-descriptor "^0.1.0"
+    define-property "^0.2.5"
+    kind-of "^3.0.3"
+
+object-hash@^1.1.4:
+  version "1.3.0"
+  resolved "https://registry.yarnpkg.com/object-hash/-/object-hash-1.3.0.tgz#76d9ba6ff113cf8efc0d996102851fe6723963e2"
+
+object-visit@^1.0.0:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/object-visit/-/object-visit-1.0.1.tgz#f79c4493af0c5377b59fe39d395e41042dd045bb"
+  dependencies:
+    isobject "^3.0.0"
+
+object.omit@^2.0.0:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/object.omit/-/object.omit-2.0.1.tgz#1a9c744829f39dbb858c76ca3579ae2a54ebd1fa"
+  dependencies:
+    for-own "^0.1.4"
+    is-extendable "^0.1.1"
+
+object.pick@^1.3.0:
+  version "1.3.0"
+  resolved "https://registry.yarnpkg.com/object.pick/-/object.pick-1.3.0.tgz#87a10ac4c1694bd2e1cbf53591a66141fb5dd747"
+  dependencies:
+    isobject "^3.0.1"
+
+on-finished@~2.3.0:
+  version "2.3.0"
+  resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947"
+  dependencies:
+    ee-first "1.1.1"
+
+once@1.x, once@^1.3.0, once@^1.3.1, once@^1.3.3, once@^1.4.0:
+  version "1.4.0"
+  resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1"
+  dependencies:
+    wrappy "1"
+
+onetime@^1.0.0:
+  version "1.1.0"
+  resolved "http://registry.npmjs.org/onetime/-/onetime-1.1.0.tgz#a1f7838f8314c516f05ecefcbc4ccfe04b4ed789"
+
+onetime@^2.0.0:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/onetime/-/onetime-2.0.1.tgz#067428230fd67443b2794b22bba528b6867962d4"
+  dependencies:
+    mimic-fn "^1.0.0"
+
+opener@^1.4.3:
+  version "1.4.3"
+  resolved "https://registry.yarnpkg.com/opener/-/opener-1.4.3.tgz#5c6da2c5d7e5831e8ffa3964950f8d6674ac90b8"
+
+opn@^4.0.2:
+  version "4.0.2"
+  resolved "https://registry.yarnpkg.com/opn/-/opn-4.0.2.tgz#7abc22e644dff63b0a96d5ab7f2790c0f01abc95"
+  dependencies:
+    object-assign "^4.0.1"
+    pinkie-promise "^2.0.0"
+
+optimist@0.6.1, optimist@^0.6.1:
+  version "0.6.1"
+  resolved "https://registry.yarnpkg.com/optimist/-/optimist-0.6.1.tgz#da3ea74686fa21a19a111c326e90eb15a0196686"
+  dependencies:
+    minimist "~0.0.1"
+    wordwrap "~0.0.2"
+
+optimize-css-assets-webpack-plugin@^1.3.0:
+  version "1.3.2"
+  resolved "https://registry.yarnpkg.com/optimize-css-assets-webpack-plugin/-/optimize-css-assets-webpack-plugin-1.3.2.tgz#eb27456e21eefbd8080f31e8368c59684e585a2c"
+  dependencies:
+    cssnano "^3.4.0"
+    underscore "^1.8.3"
+    webpack-sources "^0.1.0"
+
+optionator@^0.8.1, optionator@^0.8.2:
+  version "0.8.2"
+  resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.8.2.tgz#364c5e409d3f4d6301d6c0b4c05bba50180aeb64"
+  dependencies:
+    deep-is "~0.1.3"
+    fast-levenshtein "~2.0.4"
+    levn "~0.3.0"
+    prelude-ls "~1.1.2"
+    type-check "~0.3.2"
+    wordwrap "~1.0.0"
+
+options@>=0.0.5:
+  version "0.0.6"
+  resolved "https://registry.yarnpkg.com/options/-/options-0.0.6.tgz#ec22d312806bb53e731773e7cdaefcf1c643128f"
+
+ora@^1.2.0:
+  version "1.4.0"
+  resolved "https://registry.yarnpkg.com/ora/-/ora-1.4.0.tgz#884458215b3a5d4097592285f93321bb7a79e2e5"
+  dependencies:
+    chalk "^2.1.0"
+    cli-cursor "^2.1.0"
+    cli-spinners "^1.0.1"
+    log-symbols "^2.1.0"
+
+os-browserify@^0.3.0:
+  version "0.3.0"
+  resolved "https://registry.yarnpkg.com/os-browserify/-/os-browserify-0.3.0.tgz#854373c7f5c2315914fc9bfc6bd8238fdda1ec27"
+
+os-homedir@^1.0.0, os-homedir@^1.0.1:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/os-homedir/-/os-homedir-1.0.2.tgz#ffbc4988336e0e833de0c168c7ef152121aa7fb3"
+
+os-locale@^1.4.0:
+  version "1.4.0"
+  resolved "https://registry.yarnpkg.com/os-locale/-/os-locale-1.4.0.tgz#20f9f17ae29ed345e8bde583b13d2009803c14d9"
+  dependencies:
+    lcid "^1.0.0"
+
+os-locale@^2.0.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/os-locale/-/os-locale-2.1.0.tgz#42bc2900a6b5b8bd17376c8e882b65afccf24bf2"
+  dependencies:
+    execa "^0.7.0"
+    lcid "^1.0.0"
+    mem "^1.1.0"
+
+os-tmpdir@^1.0.0, os-tmpdir@^1.0.1, os-tmpdir@~1.0.1, os-tmpdir@~1.0.2:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274"
+
+osenv@0, osenv@^0.1.4:
+  version "0.1.5"
+  resolved "https://registry.yarnpkg.com/osenv/-/osenv-0.1.5.tgz#85cdfafaeb28e8677f416e287592b5f3f49ea410"
+  dependencies:
+    os-homedir "^1.0.0"
+    os-tmpdir "^1.0.0"
+
+outlayer@^2.1.0:
+  version "2.1.1"
+  resolved "https://registry.yarnpkg.com/outlayer/-/outlayer-2.1.1.tgz#29863b6de10ea5dadfffcadfa0d728907387e9a2"
+  dependencies:
+    ev-emitter "^1.0.0"
+    fizzy-ui-utils "^2.0.0"
+    get-size "^2.0.2"
+
+p-finally@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-1.0.0.tgz#3fbcfb15b899a44123b34b6dcc18b724336a2cae"
+
+p-limit@^1.0.0, p-limit@^1.1.0:
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-1.2.0.tgz#0e92b6bedcb59f022c13d0f1949dc82d15909f1c"
+  dependencies:
+    p-try "^1.0.0"
+
+p-locate@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-2.0.0.tgz#20a0103b222a70c8fd39cc2e580680f3dde5ec43"
+  dependencies:
+    p-limit "^1.1.0"
+
+p-map@^1.1.1:
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/p-map/-/p-map-1.2.0.tgz#e4e94f311eabbc8633a1e79908165fca26241b6b"
+
+p-try@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/p-try/-/p-try-1.0.0.tgz#cbc79cdbaf8fd4228e13f621f2b1a237c1b207b3"
+
+pac-proxy-agent@1:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/pac-proxy-agent/-/pac-proxy-agent-1.1.0.tgz#34a385dfdf61d2f0ecace08858c745d3e791fd4d"
+  dependencies:
+    agent-base "2"
+    debug "2"
+    extend "3"
+    get-uri "2"
+    http-proxy-agent "1"
+    https-proxy-agent "1"
+    pac-resolver "~2.0.0"
+    raw-body "2"
+    socks-proxy-agent "2"
+
+pac-resolver@~2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/pac-resolver/-/pac-resolver-2.0.0.tgz#99b88d2f193fbdeefc1c9a529c1f3260ab5277cd"
+  dependencies:
+    co "~3.0.6"
+    degenerator "~1.0.2"
+    ip "1.0.1"
+    netmask "~1.0.4"
+    thunkify "~2.1.1"
+
+pako@~1.0.5:
+  version "1.0.6"
+  resolved "https://registry.yarnpkg.com/pako/-/pako-1.0.6.tgz#0101211baa70c4bca4a0f63f2206e97b7dfaf258"
+
+parallel-transform@^1.1.0:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/parallel-transform/-/parallel-transform-1.1.0.tgz#d410f065b05da23081fcd10f28854c29bda33b06"
+  dependencies:
+    cyclist "~0.2.2"
+    inherits "^2.0.3"
+    readable-stream "^2.1.5"
+
+param-case@2.1.x:
+  version "2.1.1"
+  resolved "https://registry.yarnpkg.com/param-case/-/param-case-2.1.1.tgz#df94fd8cf6531ecf75e6bef9a0858fbc72be2247"
+  dependencies:
+    no-case "^2.2.0"
+
+parse-asn1@^5.0.0:
+  version "5.1.0"
+  resolved "https://registry.yarnpkg.com/parse-asn1/-/parse-asn1-5.1.0.tgz#37c4f9b7ed3ab65c74817b5f2480937fbf97c712"
+  dependencies:
+    asn1.js "^4.0.0"
+    browserify-aes "^1.0.0"
+    create-hash "^1.1.0"
+    evp_bytestokey "^1.0.0"
+    pbkdf2 "^3.0.3"
+
+parse-glob@^3.0.4:
+  version "3.0.4"
+  resolved "https://registry.yarnpkg.com/parse-glob/-/parse-glob-3.0.4.tgz#b2c376cfb11f35513badd173ef0bb6e3a388391c"
+  dependencies:
+    glob-base "^0.3.0"
+    is-dotfile "^1.0.0"
+    is-extglob "^1.0.0"
+    is-glob "^2.0.0"
+
+parse-json@^2.2.0:
+  version "2.2.0"
+  resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-2.2.0.tgz#f480f40434ef80741f8469099f8dea18f55a4dc9"
+  dependencies:
+    error-ex "^1.2.0"
+
+parsejson@0.0.3:
+  version "0.0.3"
+  resolved "https://registry.yarnpkg.com/parsejson/-/parsejson-0.0.3.tgz#ab7e3759f209ece99437973f7d0f1f64ae0e64ab"
+  dependencies:
+    better-assert "~1.0.0"
+
+parseqs@0.0.5:
+  version "0.0.5"
+  resolved "https://registry.yarnpkg.com/parseqs/-/parseqs-0.0.5.tgz#d5208a3738e46766e291ba2ea173684921a8b89d"
+  dependencies:
+    better-assert "~1.0.0"
+
+parseuri@0.0.5:
+  version "0.0.5"
+  resolved "https://registry.yarnpkg.com/parseuri/-/parseuri-0.0.5.tgz#80204a50d4dbb779bfdc6ebe2778d90e4bce320a"
+  dependencies:
+    better-assert "~1.0.0"
+
+parseurl@~1.3.2:
+  version "1.3.2"
+  resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.2.tgz#fc289d4ed8993119460c156253262cdc8de65bf3"
+
+pascalcase@^0.1.1:
+  version "0.1.1"
+  resolved "https://registry.yarnpkg.com/pascalcase/-/pascalcase-0.1.1.tgz#b363e55e8006ca6fe21784d2db22bd15d7917f14"
+
+path-browserify@0.0.0:
+  version "0.0.0"
+  resolved "https://registry.yarnpkg.com/path-browserify/-/path-browserify-0.0.0.tgz#a0b870729aae214005b7d5032ec2cbbb0fb4451a"
+
+path-dirname@^1.0.0:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/path-dirname/-/path-dirname-1.0.2.tgz#cc33d24d525e099a5388c0336c6e32b9160609e0"
+
+path-exists@^2.0.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-2.1.0.tgz#0feb6c64f0fc518d9a754dd5efb62c7022761f4b"
+  dependencies:
+    pinkie-promise "^2.0.0"
+
+path-exists@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-3.0.0.tgz#ce0ebeaa5f78cb18925ea7d810d7b59b010fd515"
+
+path-is-absolute@^1.0.0, path-is-absolute@^1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f"
+
+path-is-inside@^1.0.1:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/path-is-inside/-/path-is-inside-1.0.2.tgz#365417dede44430d1c11af61027facf074bdfc53"
+
+path-key@^2.0.0:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/path-key/-/path-key-2.0.1.tgz#411cadb574c5a140d3a4b1910d40d80cc9f40b40"
+
+path-parse@^1.0.5:
+  version "1.0.5"
+  resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.5.tgz#3c1adf871ea9cd6c9431b6ea2bd74a0ff055c4c1"
+
+path-to-regexp@0.1.7:
+  version "0.1.7"
+  resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c"
+
+path-to-regexp@^1.7.0:
+  version "1.7.0"
+  resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-1.7.0.tgz#59fde0f435badacba103a84e9d3bc64e96b9937d"
+  dependencies:
+    isarray "0.0.1"
+
+path-type@^1.0.0:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/path-type/-/path-type-1.1.0.tgz#59c44f7ee491da704da415da5a4070ba4f8fe441"
+  dependencies:
+    graceful-fs "^4.1.2"
+    pify "^2.0.0"
+    pinkie-promise "^2.0.0"
+
+path-type@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/path-type/-/path-type-2.0.0.tgz#f012ccb8415b7096fc2daa1054c3d72389594c73"
+  dependencies:
+    pify "^2.0.0"
+
+path-type@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/path-type/-/path-type-3.0.0.tgz#cef31dc8e0a1a3bb0d105c0cd97cf3bf47f4e36f"
+  dependencies:
+    pify "^3.0.0"
+
+pbkdf2@^3.0.3:
+  version "3.0.14"
+  resolved "https://registry.yarnpkg.com/pbkdf2/-/pbkdf2-3.0.14.tgz#a35e13c64799b06ce15320f459c230e68e73bade"
+  dependencies:
+    create-hash "^1.1.2"
+    create-hmac "^1.1.4"
+    ripemd160 "^2.0.1"
+    safe-buffer "^5.0.1"
+    sha.js "^2.4.8"
+
+pend@~1.2.0:
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/pend/-/pend-1.2.0.tgz#7a57eb550a6783f9115331fcf4663d5c8e007a50"
+
+performance-now@^0.2.0:
+  version "0.2.0"
+  resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-0.2.0.tgz#33ef30c5c77d4ea21c5a53869d91b56d8f2555e5"
+
+performance-now@^2.1.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b"
+
+phantomjs-prebuilt@^2.1.14, phantomjs-prebuilt@^2.1.7:
+  version "2.1.16"
+  resolved "https://registry.yarnpkg.com/phantomjs-prebuilt/-/phantomjs-prebuilt-2.1.16.tgz#efd212a4a3966d3647684ea8ba788549be2aefef"
+  dependencies:
+    es6-promise "^4.0.3"
+    extract-zip "^1.6.5"
+    fs-extra "^1.0.0"
+    hasha "^2.2.0"
+    kew "^0.7.0"
+    progress "^1.1.8"
+    request "^2.81.0"
+    request-progress "^2.0.1"
+    which "^1.2.10"
+
+pify@^2.0.0:
+  version "2.3.0"
+  resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c"
+
+pify@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/pify/-/pify-3.0.0.tgz#e5a4acd2c101fdf3d9a4d07f0dbc4db49dd28176"
+
+pinkie-promise@^2.0.0:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/pinkie-promise/-/pinkie-promise-2.0.1.tgz#2135d6dfa7a358c069ac9b178776288228450ffa"
+  dependencies:
+    pinkie "^2.0.0"
+
+pinkie@^2.0.0:
+  version "2.0.4"
+  resolved "https://registry.yarnpkg.com/pinkie/-/pinkie-2.0.4.tgz#72556b80cfa0d48a974e80e77248e80ed4f7f870"
+
+pkg-dir@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-1.0.0.tgz#7a4b508a8d5bb2d629d447056ff4e9c9314cf3d4"
+  dependencies:
+    find-up "^1.0.0"
+
+pkg-dir@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-2.0.0.tgz#f6d5d1109e19d63edf428e0bd57e12777615334b"
+  dependencies:
+    find-up "^2.1.0"
+
+pluralize@^1.2.1:
+  version "1.2.1"
+  resolved "https://registry.yarnpkg.com/pluralize/-/pluralize-1.2.1.tgz#d1a21483fd22bb41e58a12fa3421823140897c45"
+
+pofile@^1.0.2:
+  version "1.0.10"
+  resolved "https://registry.yarnpkg.com/pofile/-/pofile-1.0.10.tgz#503dda9499403984e83ff4489ba2d80af276172a"
+
+posix-character-classes@^0.1.0:
+  version "0.1.1"
+  resolved "https://registry.yarnpkg.com/posix-character-classes/-/posix-character-classes-0.1.1.tgz#01eac0fe3b5af71a2a6c02feabb8c1fef7e00eab"
+
+postcss-calc@^5.2.0:
+  version "5.3.1"
+  resolved "https://registry.yarnpkg.com/postcss-calc/-/postcss-calc-5.3.1.tgz#77bae7ca928ad85716e2fda42f261bf7c1d65b5e"
+  dependencies:
+    postcss "^5.0.2"
+    postcss-message-helpers "^2.0.0"
+    reduce-css-calc "^1.2.6"
+
+postcss-colormin@^2.1.8:
+  version "2.2.2"
+  resolved "https://registry.yarnpkg.com/postcss-colormin/-/postcss-colormin-2.2.2.tgz#6631417d5f0e909a3d7ec26b24c8a8d1e4f96e4b"
+  dependencies:
+    colormin "^1.0.5"
+    postcss "^5.0.13"
+    postcss-value-parser "^3.2.3"
+
+postcss-convert-values@^2.3.4:
+  version "2.6.1"
+  resolved "https://registry.yarnpkg.com/postcss-convert-values/-/postcss-convert-values-2.6.1.tgz#bbd8593c5c1fd2e3d1c322bb925dcae8dae4d62d"
+  dependencies:
+    postcss "^5.0.11"
+    postcss-value-parser "^3.1.2"
+
+postcss-discard-comments@^2.0.4:
+  version "2.0.4"
+  resolved "https://registry.yarnpkg.com/postcss-discard-comments/-/postcss-discard-comments-2.0.4.tgz#befe89fafd5b3dace5ccce51b76b81514be00e3d"
+  dependencies:
+    postcss "^5.0.14"
+
+postcss-discard-duplicates@^2.0.1:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/postcss-discard-duplicates/-/postcss-discard-duplicates-2.1.0.tgz#b9abf27b88ac188158a5eb12abcae20263b91932"
+  dependencies:
+    postcss "^5.0.4"
+
+postcss-discard-empty@^2.0.1:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/postcss-discard-empty/-/postcss-discard-empty-2.1.0.tgz#d2b4bd9d5ced5ebd8dcade7640c7d7cd7f4f92b5"
+  dependencies:
+    postcss "^5.0.14"
+
+postcss-discard-overridden@^0.1.1:
+  version "0.1.1"
+  resolved "https://registry.yarnpkg.com/postcss-discard-overridden/-/postcss-discard-overridden-0.1.1.tgz#8b1eaf554f686fb288cd874c55667b0aa3668d58"
+  dependencies:
+    postcss "^5.0.16"
+
+postcss-discard-unused@^2.2.1:
+  version "2.2.3"
+  resolved "https://registry.yarnpkg.com/postcss-discard-unused/-/postcss-discard-unused-2.2.3.tgz#bce30b2cc591ffc634322b5fb3464b6d934f4433"
+  dependencies:
+    postcss "^5.0.14"
+    uniqs "^2.0.0"
+
+postcss-filter-plugins@^2.0.0:
+  version "2.0.2"
+  resolved "https://registry.yarnpkg.com/postcss-filter-plugins/-/postcss-filter-plugins-2.0.2.tgz#6d85862534d735ac420e4a85806e1f5d4286d84c"
+  dependencies:
+    postcss "^5.0.4"
+    uniqid "^4.0.0"
+
+postcss-load-config@^1.1.0:
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/postcss-load-config/-/postcss-load-config-1.2.0.tgz#539e9afc9ddc8620121ebf9d8c3673e0ce50d28a"
+  dependencies:
+    cosmiconfig "^2.1.0"
+    object-assign "^4.1.0"
+    postcss-load-options "^1.2.0"
+    postcss-load-plugins "^2.3.0"
+
+postcss-load-options@^1.2.0:
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/postcss-load-options/-/postcss-load-options-1.2.0.tgz#b098b1559ddac2df04bc0bb375f99a5cfe2b6d8c"
+  dependencies:
+    cosmiconfig "^2.1.0"
+    object-assign "^4.1.0"
+
+postcss-load-plugins@^2.3.0:
+  version "2.3.0"
+  resolved "https://registry.yarnpkg.com/postcss-load-plugins/-/postcss-load-plugins-2.3.0.tgz#745768116599aca2f009fad426b00175049d8d92"
+  dependencies:
+    cosmiconfig "^2.1.1"
+    object-assign "^4.1.0"
+
+postcss-merge-idents@^2.1.5:
+  version "2.1.7"
+  resolved "https://registry.yarnpkg.com/postcss-merge-idents/-/postcss-merge-idents-2.1.7.tgz#4c5530313c08e1d5b3bbf3d2bbc747e278eea270"
+  dependencies:
+    has "^1.0.1"
+    postcss "^5.0.10"
+    postcss-value-parser "^3.1.1"
+
+postcss-merge-longhand@^2.0.1:
+  version "2.0.2"
+  resolved "https://registry.yarnpkg.com/postcss-merge-longhand/-/postcss-merge-longhand-2.0.2.tgz#23d90cd127b0a77994915332739034a1a4f3d658"
+  dependencies:
+    postcss "^5.0.4"
+
+postcss-merge-rules@^2.0.3:
+  version "2.1.2"
+  resolved "https://registry.yarnpkg.com/postcss-merge-rules/-/postcss-merge-rules-2.1.2.tgz#d1df5dfaa7b1acc3be553f0e9e10e87c61b5f721"
+  dependencies:
+    browserslist "^1.5.2"
+    caniuse-api "^1.5.2"
+    postcss "^5.0.4"
+    postcss-selector-parser "^2.2.2"
+    vendors "^1.0.0"
+
+postcss-message-helpers@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/postcss-message-helpers/-/postcss-message-helpers-2.0.0.tgz#a4f2f4fab6e4fe002f0aed000478cdf52f9ba60e"
+
+postcss-minify-font-values@^1.0.2:
+  version "1.0.5"
+  resolved "https://registry.yarnpkg.com/postcss-minify-font-values/-/postcss-minify-font-values-1.0.5.tgz#4b58edb56641eba7c8474ab3526cafd7bbdecb69"
+  dependencies:
+    object-assign "^4.0.1"
+    postcss "^5.0.4"
+    postcss-value-parser "^3.0.2"
+
+postcss-minify-gradients@^1.0.1:
+  version "1.0.5"
+  resolved "https://registry.yarnpkg.com/postcss-minify-gradients/-/postcss-minify-gradients-1.0.5.tgz#5dbda11373703f83cfb4a3ea3881d8d75ff5e6e1"
+  dependencies:
+    postcss "^5.0.12"
+    postcss-value-parser "^3.3.0"
+
+postcss-minify-params@^1.0.4:
+  version "1.2.2"
+  resolved "https://registry.yarnpkg.com/postcss-minify-params/-/postcss-minify-params-1.2.2.tgz#ad2ce071373b943b3d930a3fa59a358c28d6f1f3"
+  dependencies:
+    alphanum-sort "^1.0.1"
+    postcss "^5.0.2"
+    postcss-value-parser "^3.0.2"
+    uniqs "^2.0.0"
+
+postcss-minify-selectors@^2.0.4:
+  version "2.1.1"
+  resolved "https://registry.yarnpkg.com/postcss-minify-selectors/-/postcss-minify-selectors-2.1.1.tgz#b2c6a98c0072cf91b932d1a496508114311735bf"
+  dependencies:
+    alphanum-sort "^1.0.2"
+    has "^1.0.1"
+    postcss "^5.0.14"
+    postcss-selector-parser "^2.0.0"
+
+postcss-modules-extract-imports@^1.2.0:
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/postcss-modules-extract-imports/-/postcss-modules-extract-imports-1.2.0.tgz#66140ecece38ef06bf0d3e355d69bf59d141ea85"
+  dependencies:
+    postcss "^6.0.1"
+
+postcss-modules-local-by-default@^1.2.0:
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/postcss-modules-local-by-default/-/postcss-modules-local-by-default-1.2.0.tgz#f7d80c398c5a393fa7964466bd19500a7d61c069"
+  dependencies:
+    css-selector-tokenizer "^0.7.0"
+    postcss "^6.0.1"
+
+postcss-modules-scope@^1.1.0:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/postcss-modules-scope/-/postcss-modules-scope-1.1.0.tgz#d6ea64994c79f97b62a72b426fbe6056a194bb90"
+  dependencies:
+    css-selector-tokenizer "^0.7.0"
+    postcss "^6.0.1"
+
+postcss-modules-values@^1.3.0:
+  version "1.3.0"
+  resolved "https://registry.yarnpkg.com/postcss-modules-values/-/postcss-modules-values-1.3.0.tgz#ecffa9d7e192518389f42ad0e83f72aec456ea20"
+  dependencies:
+    icss-replace-symbols "^1.1.0"
+    postcss "^6.0.1"
+
+postcss-normalize-charset@^1.1.0:
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/postcss-normalize-charset/-/postcss-normalize-charset-1.1.1.tgz#ef9ee71212d7fe759c78ed162f61ed62b5cb93f1"
+  dependencies:
+    postcss "^5.0.5"
+
+postcss-normalize-url@^3.0.7:
+  version "3.0.8"
+  resolved "https://registry.yarnpkg.com/postcss-normalize-url/-/postcss-normalize-url-3.0.8.tgz#108f74b3f2fcdaf891a2ffa3ea4592279fc78222"
+  dependencies:
+    is-absolute-url "^2.0.0"
+    normalize-url "^1.4.0"
+    postcss "^5.0.14"
+    postcss-value-parser "^3.2.3"
+
+postcss-ordered-values@^2.1.0:
+  version "2.2.3"
+  resolved "https://registry.yarnpkg.com/postcss-ordered-values/-/postcss-ordered-values-2.2.3.tgz#eec6c2a67b6c412a8db2042e77fe8da43f95c11d"
+  dependencies:
+    postcss "^5.0.4"
+    postcss-value-parser "^3.0.1"
+
+postcss-reduce-idents@^2.2.2:
+  version "2.4.0"
+  resolved "https://registry.yarnpkg.com/postcss-reduce-idents/-/postcss-reduce-idents-2.4.0.tgz#c2c6d20cc958284f6abfbe63f7609bf409059ad3"
+  dependencies:
+    postcss "^5.0.4"
+    postcss-value-parser "^3.0.2"
+
+postcss-reduce-initial@^1.0.0:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/postcss-reduce-initial/-/postcss-reduce-initial-1.0.1.tgz#68f80695f045d08263a879ad240df8dd64f644ea"
+  dependencies:
+    postcss "^5.0.4"
+
+postcss-reduce-transforms@^1.0.3:
+  version "1.0.4"
+  resolved "https://registry.yarnpkg.com/postcss-reduce-transforms/-/postcss-reduce-transforms-1.0.4.tgz#ff76f4d8212437b31c298a42d2e1444025771ae1"
+  dependencies:
+    has "^1.0.1"
+    postcss "^5.0.8"
+    postcss-value-parser "^3.0.1"
+
+postcss-selector-parser@^2.0.0, postcss-selector-parser@^2.2.2:
+  version "2.2.3"
+  resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-2.2.3.tgz#f9437788606c3c9acee16ffe8d8b16297f27bb90"
+  dependencies:
+    flatten "^1.0.2"
+    indexes-of "^1.0.1"
+    uniq "^1.0.1"
+
+postcss-svgo@^2.1.1:
+  version "2.1.6"
+  resolved "https://registry.yarnpkg.com/postcss-svgo/-/postcss-svgo-2.1.6.tgz#b6df18aa613b666e133f08adb5219c2684ac108d"
+  dependencies:
+    is-svg "^2.0.0"
+    postcss "^5.0.14"
+    postcss-value-parser "^3.2.3"
+    svgo "^0.7.0"
+
+postcss-unique-selectors@^2.0.2:
+  version "2.0.2"
+  resolved "https://registry.yarnpkg.com/postcss-unique-selectors/-/postcss-unique-selectors-2.0.2.tgz#981d57d29ddcb33e7b1dfe1fd43b8649f933ca1d"
+  dependencies:
+    alphanum-sort "^1.0.1"
+    postcss "^5.0.4"
+    uniqs "^2.0.0"
+
+postcss-value-parser@^3.0.1, postcss-value-parser@^3.0.2, postcss-value-parser@^3.1.1, postcss-value-parser@^3.1.2, postcss-value-parser@^3.2.3, postcss-value-parser@^3.3.0:
+  version "3.3.0"
+  resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-3.3.0.tgz#87f38f9f18f774a4ab4c8a232f5c5ce8872a9d15"
+
+postcss-zindex@^2.0.1:
+  version "2.2.0"
+  resolved "https://registry.yarnpkg.com/postcss-zindex/-/postcss-zindex-2.2.0.tgz#d2109ddc055b91af67fc4cb3b025946639d2af22"
+  dependencies:
+    has "^1.0.1"
+    postcss "^5.0.4"
+    uniqs "^2.0.0"
+
+postcss@^5.0.10, postcss@^5.0.11, postcss@^5.0.12, postcss@^5.0.13, postcss@^5.0.14, postcss@^5.0.16, postcss@^5.0.2, postcss@^5.0.21, postcss@^5.0.4, postcss@^5.0.5, postcss@^5.0.6, postcss@^5.0.8, postcss@^5.2.16:
+  version "5.2.18"
+  resolved "https://registry.yarnpkg.com/postcss/-/postcss-5.2.18.tgz#badfa1497d46244f6390f58b319830d9107853c5"
+  dependencies:
+    chalk "^1.1.3"
+    js-base64 "^2.1.9"
+    source-map "^0.5.6"
+    supports-color "^3.2.3"
+
+postcss@^6.0.1:
+  version "6.0.21"
+  resolved "https://registry.yarnpkg.com/postcss/-/postcss-6.0.21.tgz#8265662694eddf9e9a5960db6da33c39e4cd069d"
+  dependencies:
+    chalk "^2.3.2"
+    source-map "^0.6.1"
+    supports-color "^5.3.0"
+
+prelude-ls@~1.1.2:
+  version "1.1.2"
+  resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54"
+
+prepend-http@^1.0.0:
+  version "1.0.4"
+  resolved "https://registry.yarnpkg.com/prepend-http/-/prepend-http-1.0.4.tgz#d4f4562b0ce3696e41ac52d0e002e57a635dc6dc"
+
+preserve@^0.2.0:
+  version "0.2.0"
+  resolved "https://registry.yarnpkg.com/preserve/-/preserve-0.2.0.tgz#815ed1f6ebc65926f865b310c0713bcb3315ce4b"
+
+pretty-error@^2.0.2:
+  version "2.1.1"
+  resolved "https://registry.yarnpkg.com/pretty-error/-/pretty-error-2.1.1.tgz#5f4f87c8f91e5ae3f3ba87ab4cf5e03b1a17f1a3"
+  dependencies:
+    renderkid "^2.0.1"
+    utila "~0.4"
+
+private@^0.1.6, private@^0.1.7:
+  version "0.1.8"
+  resolved "https://registry.yarnpkg.com/private/-/private-0.1.8.tgz#2381edb3689f7a53d653190060fcf822d2f368ff"
+
+process-nextick-args@~2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.0.tgz#a37d732f4271b4ab1ad070d35508e8290788ffaa"
+
+process@^0.11.10:
+  version "0.11.10"
+  resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182"
+
+progress@^1.1.8:
+  version "1.1.8"
+  resolved "https://registry.yarnpkg.com/progress/-/progress-1.1.8.tgz#e260c78f6161cdd9b0e56cc3e0a85de17c7a57be"
+
+promise-inflight@^1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/promise-inflight/-/promise-inflight-1.0.1.tgz#98472870bf228132fcbdd868129bad12c3c029e3"
+
+proto-list@~1.2.1:
+  version "1.2.4"
+  resolved "https://registry.yarnpkg.com/proto-list/-/proto-list-1.2.4.tgz#212d5bfe1318306a420f6402b8e26ff39647a849"
+
+proxy-addr@~2.0.3:
+  version "2.0.3"
+  resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.3.tgz#355f262505a621646b3130a728eb647e22055341"
+  dependencies:
+    forwarded "~0.1.2"
+    ipaddr.js "1.6.0"
+
+proxy-agent@2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/proxy-agent/-/proxy-agent-2.0.0.tgz#57eb5347aa805d74ec681cb25649dba39c933499"
+  dependencies:
+    agent-base "2"
+    debug "2"
+    extend "3"
+    http-proxy-agent "1"
+    https-proxy-agent "1"
+    lru-cache "~2.6.5"
+    pac-proxy-agent "1"
+    socks-proxy-agent "2"
+
+prr@~1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/prr/-/prr-1.0.1.tgz#d3fc114ba06995a45ec6893f484ceb1d78f5f476"
+
+pseudomap@^1.0.1, pseudomap@^1.0.2:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/pseudomap/-/pseudomap-1.0.2.tgz#f052a28da70e618917ef0a8ac34c1ae5a68286b3"
+
+public-encrypt@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/public-encrypt/-/public-encrypt-4.0.0.tgz#39f699f3a46560dd5ebacbca693caf7c65c18cc6"
+  dependencies:
+    bn.js "^4.1.0"
+    browserify-rsa "^4.0.0"
+    create-hash "^1.1.0"
+    parse-asn1 "^5.0.0"
+    randombytes "^2.0.1"
+
+pump@^2.0.0, pump@^2.0.1:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/pump/-/pump-2.0.1.tgz#12399add6e4cf7526d973cbc8b5ce2e2908b3909"
+  dependencies:
+    end-of-stream "^1.1.0"
+    once "^1.3.1"
+
+pumpify@^1.3.3:
+  version "1.4.0"
+  resolved "https://registry.yarnpkg.com/pumpify/-/pumpify-1.4.0.tgz#80b7c5df7e24153d03f0e7ac8a05a5d068bd07fb"
+  dependencies:
+    duplexify "^3.5.3"
+    inherits "^2.0.3"
+    pump "^2.0.0"
+
+punycode@1.3.2:
+  version "1.3.2"
+  resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.3.2.tgz#9653a036fb7c1ee42342f2325cceefea3926c48d"
+
+punycode@^1.2.4, punycode@^1.4.1:
+  version "1.4.1"
+  resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e"
+
+punycode@^2.1.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.0.tgz#5f863edc89b96db09074bad7947bf09056ca4e7d"
+
+q@1.4.1:
+  version "1.4.1"
+  resolved "https://registry.yarnpkg.com/q/-/q-1.4.1.tgz#55705bcd93c5f3673530c2c2cbc0c2b3addc286e"
+
+q@^1.1.2:
+  version "1.5.1"
+  resolved "https://registry.yarnpkg.com/q/-/q-1.5.1.tgz#7e32f75b41381291d04611f1bf14109ac00651d7"
+
+qjobs@^1.1.4:
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/qjobs/-/qjobs-1.2.0.tgz#c45e9c61800bd087ef88d7e256423bdd49e5d071"
+
+qs@6.5.1, qs@~6.5.1:
+  version "6.5.1"
+  resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.1.tgz#349cdf6eef89ec45c12d7d5eb3fc0c870343a6d8"
+
+qs@~6.3.0:
+  version "6.3.2"
+  resolved "https://registry.yarnpkg.com/qs/-/qs-6.3.2.tgz#e75bd5f6e268122a2a0e0bda630b2550c166502c"
+
+qs@~6.4.0:
+  version "6.4.0"
+  resolved "https://registry.yarnpkg.com/qs/-/qs-6.4.0.tgz#13e26d28ad6b0ffaa91312cd3bf708ed351e7233"
+
+query-string@^4.1.0:
+  version "4.3.4"
+  resolved "https://registry.yarnpkg.com/query-string/-/query-string-4.3.4.tgz#bbb693b9ca915c232515b228b1a02b609043dbeb"
+  dependencies:
+    object-assign "^4.1.0"
+    strict-uri-encode "^1.0.0"
+
+querystring-es3@^0.2.0:
+  version "0.2.1"
+  resolved "https://registry.yarnpkg.com/querystring-es3/-/querystring-es3-0.2.1.tgz#9ec61f79049875707d69414596fd907a4d711e73"
+
+querystring@0.2.0, querystring@^0.2.0:
+  version "0.2.0"
+  resolved "https://registry.yarnpkg.com/querystring/-/querystring-0.2.0.tgz#b209849203bb25df820da756e747005878521620"
+
+randomatic@^1.1.3:
+  version "1.1.7"
+  resolved "https://registry.yarnpkg.com/randomatic/-/randomatic-1.1.7.tgz#c7abe9cc8b87c0baa876b19fde83fd464797e38c"
+  dependencies:
+    is-number "^3.0.0"
+    kind-of "^4.0.0"
+
+randombytes@^2.0.0, randombytes@^2.0.1, randombytes@^2.0.5:
+  version "2.0.6"
+  resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.0.6.tgz#d302c522948588848a8d300c932b44c24231da80"
+  dependencies:
+    safe-buffer "^5.1.0"
+
+randomfill@^1.0.3:
+  version "1.0.4"
+  resolved "https://registry.yarnpkg.com/randomfill/-/randomfill-1.0.4.tgz#c92196fc86ab42be983f1bf31778224931d61458"
+  dependencies:
+    randombytes "^2.0.5"
+    safe-buffer "^5.1.0"
+
+range-parser@^1.0.3, range-parser@^1.2.0, range-parser@~1.2.0:
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.0.tgz#f49be6b487894ddc40dcc94a322f611092e00d5e"
+
+raven-js@^3.22.3:
+  version "3.24.0"
+  resolved "https://registry.yarnpkg.com/raven-js/-/raven-js-3.24.0.tgz#59464d8bc4b3812ae87a282e9bb98ecad5b4b047"
+
+raw-body@2, raw-body@2.3.2:
+  version "2.3.2"
+  resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.3.2.tgz#bcd60c77d3eb93cde0050295c3f379389bc88f89"
+  dependencies:
+    bytes "3.0.0"
+    http-errors "1.6.2"
+    iconv-lite "0.4.19"
+    unpipe "1.0.0"
+
+rc@^1.1.7:
+  version "1.2.6"
+  resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.6.tgz#eb18989c6d4f4f162c399f79ddd29f3835568092"
+  dependencies:
+    deep-extend "~0.4.0"
+    ini "~1.3.0"
+    minimist "^1.2.0"
+    strip-json-comments "~2.0.1"
+
+read-pkg-up@^1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-1.0.1.tgz#9d63c13276c065918d57f002a57f40a1b643fb02"
+  dependencies:
+    find-up "^1.0.0"
+    read-pkg "^1.0.0"
+
+read-pkg-up@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-2.0.0.tgz#6b72a8048984e0c41e79510fd5e9fa99b3b549be"
+  dependencies:
+    find-up "^2.0.0"
+    read-pkg "^2.0.0"
+
+read-pkg@^1.0.0:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-1.1.0.tgz#f5ffaa5ecd29cb31c0474bca7d756b6bb29e3f28"
+  dependencies:
+    load-json-file "^1.0.0"
+    normalize-package-data "^2.3.2"
+    path-type "^1.0.0"
+
+read-pkg@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-2.0.0.tgz#8ef1c0623c6a6db0dc6713c4bfac46332b2368f8"
+  dependencies:
+    load-json-file "^2.0.0"
+    normalize-package-data "^2.3.2"
+    path-type "^2.0.0"
+
+"readable-stream@1 || 2", readable-stream@2, readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.4, readable-stream@^2.0.6, readable-stream@^2.1.4, readable-stream@^2.1.5, readable-stream@^2.2.2, readable-stream@^2.3.3:
+  version "2.3.6"
+  resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.6.tgz#b11c27d88b8ff1fbe070643cf94b0c79ae1b0aaf"
+  dependencies:
+    core-util-is "~1.0.0"
+    inherits "~2.0.3"
+    isarray "~1.0.0"
+    process-nextick-args "~2.0.0"
+    safe-buffer "~5.1.1"
+    string_decoder "~1.1.1"
+    util-deprecate "~1.0.1"
+
+readable-stream@1.0, readable-stream@~1.0.2:
+  version "1.0.34"
+  resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.0.34.tgz#125820e34bc842d2f2aaafafe4c2916ee32c157c"
+  dependencies:
+    core-util-is "~1.0.0"
+    inherits "~2.0.1"
+    isarray "0.0.1"
+    string_decoder "~0.10.x"
+
+readable-stream@1.1.x:
+  version "1.1.14"
+  resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.1.14.tgz#7cf4c54ef648e3813084c636dd2079e166c081d9"
+  dependencies:
+    core-util-is "~1.0.0"
+    inherits "~2.0.1"
+    isarray "0.0.1"
+    string_decoder "~0.10.x"
+
+readdirp@^2.0.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-2.1.0.tgz#4ed0ad060df3073300c48440373f72d1cc642d78"
+  dependencies:
+    graceful-fs "^4.1.2"
+    minimatch "^3.0.2"
+    readable-stream "^2.0.2"
+    set-immediate-shim "^1.0.1"
+
+readline2@^1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/readline2/-/readline2-1.0.1.tgz#41059608ffc154757b715d9989d199ffbf372e35"
+  dependencies:
+    code-point-at "^1.0.0"
+    is-fullwidth-code-point "^1.0.0"
+    mute-stream "0.0.5"
+
+rechoir@^0.6.2:
+  version "0.6.2"
+  resolved "https://registry.yarnpkg.com/rechoir/-/rechoir-0.6.2.tgz#85204b54dba82d5742e28c96756ef43af50e3384"
+  dependencies:
+    resolve "^1.1.6"
+
+reconnecting-websocket@^3.0.3:
+  version "3.2.2"
+  resolved "https://registry.yarnpkg.com/reconnecting-websocket/-/reconnecting-websocket-3.2.2.tgz#8097514e926e9855e03c39e76efa2e3d1f371bee"
+
+redent@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/redent/-/redent-1.0.0.tgz#cf916ab1fd5f1f16dfb20822dd6ec7f730c2afde"
+  dependencies:
+    indent-string "^2.1.0"
+    strip-indent "^1.0.1"
+
+reduce-css-calc@^1.2.6:
+  version "1.3.0"
+  resolved "https://registry.yarnpkg.com/reduce-css-calc/-/reduce-css-calc-1.3.0.tgz#747c914e049614a4c9cfbba629871ad1d2927716"
+  dependencies:
+    balanced-match "^0.4.2"
+    math-expression-evaluator "^1.2.14"
+    reduce-function-call "^1.0.1"
+
+reduce-function-call@^1.0.1:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/reduce-function-call/-/reduce-function-call-1.0.2.tgz#5a200bf92e0e37751752fe45b0ab330fd4b6be99"
+  dependencies:
+    balanced-match "^0.4.2"
+
+regenerate@^1.2.1:
+  version "1.3.3"
+  resolved "https://registry.yarnpkg.com/regenerate/-/regenerate-1.3.3.tgz#0c336d3980553d755c39b586ae3b20aa49c82b7f"
+
+regenerator-runtime@^0.11.0:
+  version "0.11.1"
+  resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz#be05ad7f9bf7d22e056f9726cee5017fbf19e2e9"
+
+regenerator-transform@^0.10.0:
+  version "0.10.1"
+  resolved "https://registry.yarnpkg.com/regenerator-transform/-/regenerator-transform-0.10.1.tgz#1e4996837231da8b7f3cf4114d71b5691a0680dd"
+  dependencies:
+    babel-runtime "^6.18.0"
+    babel-types "^6.19.0"
+    private "^0.1.6"
+
+regex-cache@^0.4.2:
+  version "0.4.4"
+  resolved "https://registry.yarnpkg.com/regex-cache/-/regex-cache-0.4.4.tgz#75bdc58a2a1496cec48a12835bc54c8d562336dd"
+  dependencies:
+    is-equal-shallow "^0.1.3"
+
+regex-not@^1.0.0, regex-not@^1.0.2:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/regex-not/-/regex-not-1.0.2.tgz#1f4ece27e00b0b65e0247a6810e6a85d83a5752c"
+  dependencies:
+    extend-shallow "^3.0.2"
+    safe-regex "^1.1.0"
+
+regexpu-core@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-1.0.0.tgz#86a763f58ee4d7c2f6b102e4764050de7ed90c6b"
+  dependencies:
+    regenerate "^1.2.1"
+    regjsgen "^0.2.0"
+    regjsparser "^0.1.4"
+
+regexpu-core@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-2.0.0.tgz#49d038837b8dcf8bfa5b9a42139938e6ea2ae240"
+  dependencies:
+    regenerate "^1.2.1"
+    regjsgen "^0.2.0"
+    regjsparser "^0.1.4"
+
+regjsgen@^0.2.0:
+  version "0.2.0"
+  resolved "https://registry.yarnpkg.com/regjsgen/-/regjsgen-0.2.0.tgz#6c016adeac554f75823fe37ac05b92d5a4edb1f7"
+
+regjsparser@^0.1.4:
+  version "0.1.5"
+  resolved "https://registry.yarnpkg.com/regjsparser/-/regjsparser-0.1.5.tgz#7ee8f84dc6fa792d3fd0ae228d24bd949ead205c"
+  dependencies:
+    jsesc "~0.5.0"
+
+relateurl@0.2.x:
+  version "0.2.7"
+  resolved "https://registry.yarnpkg.com/relateurl/-/relateurl-0.2.7.tgz#54dbf377e51440aca90a4cd274600d3ff2d888a9"
+
+remove-trailing-separator@^1.0.1:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz#c24bce2a283adad5bc3f58e0d48249b92379d8ef"
+
+renderkid@^2.0.1:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/renderkid/-/renderkid-2.0.1.tgz#898cabfc8bede4b7b91135a3ffd323e58c0db319"
+  dependencies:
+    css-select "^1.1.0"
+    dom-converter "~0.1"
+    htmlparser2 "~3.3.0"
+    strip-ansi "^3.0.0"
+    utila "~0.3"
+
+repeat-element@^1.1.2:
+  version "1.1.2"
+  resolved "https://registry.yarnpkg.com/repeat-element/-/repeat-element-1.1.2.tgz#ef089a178d1483baae4d93eb98b4f9e4e11d990a"
+
+repeat-string@^0.2.2:
+  version "0.2.2"
+  resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-0.2.2.tgz#c7a8d3236068362059a7e4651fc6884e8b1fb4ae"
+
+repeat-string@^1.5.2, repeat-string@^1.6.1:
+  version "1.6.1"
+  resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-1.6.1.tgz#8dcae470e1c88abc2d600fff4a776286da75e637"
+
+repeating@^2.0.0:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/repeating/-/repeating-2.0.1.tgz#5214c53a926d3552707527fbab415dbc08d06dda"
+  dependencies:
+    is-finite "^1.0.0"
+
+request-progress@^2.0.1:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/request-progress/-/request-progress-2.0.1.tgz#5d36bb57961c673aa5b788dbc8141fdf23b44e08"
+  dependencies:
+    throttleit "^1.0.0"
+
+request@2, request@^2.81.0, request@^2.83.0:
+  version "2.85.0"
+  resolved "https://registry.yarnpkg.com/request/-/request-2.85.0.tgz#5a03615a47c61420b3eb99b7dba204f83603e1fa"
+  dependencies:
+    aws-sign2 "~0.7.0"
+    aws4 "^1.6.0"
+    caseless "~0.12.0"
+    combined-stream "~1.0.5"
+    extend "~3.0.1"
+    forever-agent "~0.6.1"
+    form-data "~2.3.1"
+    har-validator "~5.0.3"
+    hawk "~6.0.2"
+    http-signature "~1.2.0"
+    is-typedarray "~1.0.0"
+    isstream "~0.1.2"
+    json-stringify-safe "~5.0.1"
+    mime-types "~2.1.17"
+    oauth-sign "~0.8.2"
+    performance-now "^2.1.0"
+    qs "~6.5.1"
+    safe-buffer "^5.1.1"
+    stringstream "~0.0.5"
+    tough-cookie "~2.3.3"
+    tunnel-agent "^0.6.0"
+    uuid "^3.1.0"
+
+request@2.81.0:
+  version "2.81.0"
+  resolved "https://registry.yarnpkg.com/request/-/request-2.81.0.tgz#c6928946a0e06c5f8d6f8a9333469ffda46298a0"
+  dependencies:
+    aws-sign2 "~0.6.0"
+    aws4 "^1.2.1"
+    caseless "~0.12.0"
+    combined-stream "~1.0.5"
+    extend "~3.0.0"
+    forever-agent "~0.6.1"
+    form-data "~2.1.1"
+    har-validator "~4.2.1"
+    hawk "~3.1.3"
+    http-signature "~1.1.0"
+    is-typedarray "~1.0.0"
+    isstream "~0.1.2"
+    json-stringify-safe "~5.0.1"
+    mime-types "~2.1.7"
+    oauth-sign "~0.8.1"
+    performance-now "^0.2.0"
+    qs "~6.4.0"
+    safe-buffer "^5.0.1"
+    stringstream "~0.0.4"
+    tough-cookie "~2.3.0"
+    tunnel-agent "^0.6.0"
+    uuid "^3.0.0"
+
+request@~2.79.0:
+  version "2.79.0"
+  resolved "https://registry.yarnpkg.com/request/-/request-2.79.0.tgz#4dfe5bf6be8b8cdc37fcf93e04b65577722710de"
+  dependencies:
+    aws-sign2 "~0.6.0"
+    aws4 "^1.2.1"
+    caseless "~0.11.0"
+    combined-stream "~1.0.5"
+    extend "~3.0.0"
+    forever-agent "~0.6.1"
+    form-data "~2.1.1"
+    har-validator "~2.0.6"
+    hawk "~3.1.3"
+    http-signature "~1.1.0"
+    is-typedarray "~1.0.0"
+    isstream "~0.1.2"
+    json-stringify-safe "~5.0.1"
+    mime-types "~2.1.7"
+    oauth-sign "~0.8.1"
+    qs "~6.3.0"
+    stringstream "~0.0.4"
+    tough-cookie "~2.3.0"
+    tunnel-agent "~0.4.1"
+    uuid "^3.0.0"
+
+require-directory@^2.1.1:
+  version "2.1.1"
+  resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42"
+
+require-from-string@^1.1.0:
+  version "1.2.1"
+  resolved "https://registry.yarnpkg.com/require-from-string/-/require-from-string-1.2.1.tgz#529c9ccef27380adfec9a2f965b649bbee636418"
+
+require-main-filename@^1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-1.0.1.tgz#97f717b69d48784f5f526a6c5aa8ffdda055a4d1"
+
+require-uncached@^1.0.2:
+  version "1.0.3"
+  resolved "https://registry.yarnpkg.com/require-uncached/-/require-uncached-1.0.3.tgz#4e0d56d6c9662fd31e43011c4b95aa49955421d3"
+  dependencies:
+    caller-path "^0.1.0"
+    resolve-from "^1.0.0"
+
+requires-port@1.x.x:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff"
+
+resolve-from@^1.0.0:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-1.0.1.tgz#26cbfe935d1aeeeabb29bc3fe5aeb01e93d44226"
+
+resolve-url@^0.2.1:
+  version "0.2.1"
+  resolved "https://registry.yarnpkg.com/resolve-url/-/resolve-url-0.2.1.tgz#2c637fe77c893afd2a663fe21aa9080068e2052a"
+
+resolve@1.1.x:
+  version "1.1.7"
+  resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.1.7.tgz#203114d82ad2c5ed9e8e0411b3932875e889e97b"
+
+resolve@^1.1.6, resolve@^1.3.3:
+  version "1.7.0"
+  resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.7.0.tgz#2bdf5374811207285df0df652b78f118ab8f3c5e"
+  dependencies:
+    path-parse "^1.0.5"
+
+restore-cursor@^1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-1.0.1.tgz#34661f46886327fed2991479152252df92daa541"
+  dependencies:
+    exit-hook "^1.0.0"
+    onetime "^1.0.0"
+
+restore-cursor@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-2.0.0.tgz#9f7ee287f82fd326d4fd162923d62129eee0dfaf"
+  dependencies:
+    onetime "^2.0.0"
+    signal-exit "^3.0.2"
+
+ret@~0.1.10:
+  version "0.1.15"
+  resolved "https://registry.yarnpkg.com/ret/-/ret-0.1.15.tgz#b8a4825d5bdb1fc3f6f53c2bc33f81388681c7bc"
+
+right-align@^0.1.1:
+  version "0.1.3"
+  resolved "https://registry.yarnpkg.com/right-align/-/right-align-0.1.3.tgz#61339b722fe6a3515689210d24e14c96148613ef"
+  dependencies:
+    align-text "^0.1.1"
+
+rimraf@2, rimraf@^2.2.8, rimraf@^2.5.1, rimraf@^2.5.4, rimraf@^2.6.0, rimraf@^2.6.1, rimraf@^2.6.2:
+  version "2.6.2"
+  resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.2.tgz#2ed8150d24a16ea8651e6d6ef0f47c4158ce7a36"
+  dependencies:
+    glob "^7.0.5"
+
+ripemd160@^2.0.0, ripemd160@^2.0.1:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/ripemd160/-/ripemd160-2.0.1.tgz#0f4584295c53a3628af7e6d79aca21ce57d1c6e7"
+  dependencies:
+    hash-base "^2.0.0"
+    inherits "^2.0.1"
+
+run-async@^0.1.0:
+  version "0.1.0"
+  resolved "https://registry.yarnpkg.com/run-async/-/run-async-0.1.0.tgz#c8ad4a5e110661e402a7d21b530e009f25f8e389"
+  dependencies:
+    once "^1.3.0"
+
+run-queue@^1.0.0, run-queue@^1.0.3:
+  version "1.0.3"
+  resolved "https://registry.yarnpkg.com/run-queue/-/run-queue-1.0.3.tgz#e848396f057d223f24386924618e25694161ec47"
+  dependencies:
+    aproba "^1.1.1"
+
+rx-lite@^3.1.2:
+  version "3.1.2"
+  resolved "https://registry.yarnpkg.com/rx-lite/-/rx-lite-3.1.2.tgz#19ce502ca572665f3b647b10939f97fd1615f102"
+
+safe-buffer@5.1.1, safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, safe-buffer@~5.1.0, safe-buffer@~5.1.1:
+  version "5.1.1"
+  resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.1.tgz#893312af69b2123def71f57889001671eeb2c853"
+
+safe-regex@^1.1.0:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/safe-regex/-/safe-regex-1.1.0.tgz#40a3669f3b077d1e943d44629e157dd48023bf2e"
+  dependencies:
+    ret "~0.1.10"
+
+safer-buffer@^2.1.0:
+  version "2.1.2"
+  resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
+
+samsam@1.x, samsam@^1.1.3:
+  version "1.3.0"
+  resolved "https://registry.yarnpkg.com/samsam/-/samsam-1.3.0.tgz#8d1d9350e25622da30de3e44ba692b5221ab7c50"
+
+sass-graph@^2.2.4:
+  version "2.2.4"
+  resolved "https://registry.yarnpkg.com/sass-graph/-/sass-graph-2.2.4.tgz#13fbd63cd1caf0908b9fd93476ad43a51d1e0b49"
+  dependencies:
+    glob "^7.0.0"
+    lodash "^4.0.0"
+    scss-tokenizer "^0.2.3"
+    yargs "^7.0.0"
+
+sass-loader@^6.0.5:
+  version "6.0.7"
+  resolved "https://registry.yarnpkg.com/sass-loader/-/sass-loader-6.0.7.tgz#dd2fdb3e7eeff4a53f35ba6ac408715488353d00"
+  dependencies:
+    clone-deep "^2.0.1"
+    loader-utils "^1.0.1"
+    lodash.tail "^4.1.1"
+    neo-async "^2.5.0"
+    pify "^3.0.0"
+
+sax@~1.2.1:
+  version "1.2.4"
+  resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9"
+
+schema-utils@^0.3.0:
+  version "0.3.0"
+  resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-0.3.0.tgz#f5877222ce3e931edae039f17eb3716e7137f8cf"
+  dependencies:
+    ajv "^5.0.0"
+
+scss-tokenizer@^0.2.3:
+  version "0.2.3"
+  resolved "https://registry.yarnpkg.com/scss-tokenizer/-/scss-tokenizer-0.2.3.tgz#8eb06db9a9723333824d3f5530641149847ce5d1"
+  dependencies:
+    js-base64 "^2.1.8"
+    source-map "^0.4.2"
+
+selenium-server@^3.0.1:
+  version "3.11.0"
+  resolved "https://registry.yarnpkg.com/selenium-server/-/selenium-server-3.11.0.tgz#fb20b049805d6a663ab4a609dd505fd9eddd7d15"
+
+semantic-ui-css@^2.2.10:
+  version "2.3.1"
+  resolved "https://registry.yarnpkg.com/semantic-ui-css/-/semantic-ui-css-2.3.1.tgz#a5485c640c98cce29d8ddde3eff3434566a068e0"
+  dependencies:
+    jquery x.*
+
+"semver@2 || 3 || 4 || 5", semver@^5.1.0, semver@^5.3.0:
+  version "5.5.0"
+  resolved "https://registry.yarnpkg.com/semver/-/semver-5.5.0.tgz#dc4bbc7a6ca9d916dee5d43516f0092b58f7b8ab"
+
+semver@~4.3.3:
+  version "4.3.6"
+  resolved "https://registry.yarnpkg.com/semver/-/semver-4.3.6.tgz#300bc6e0e86374f7ba61068b5b1ecd57fc6532da"
+
+semver@~5.0.1:
+  version "5.0.3"
+  resolved "https://registry.yarnpkg.com/semver/-/semver-5.0.3.tgz#77466de589cd5d3c95f138aa78bc569a3cb5d27a"
+
+semver@~5.3.0:
+  version "5.3.0"
+  resolved "https://registry.yarnpkg.com/semver/-/semver-5.3.0.tgz#9b2ce5d3de02d17c6012ad326aa6b4d0cf54f94f"
+
+send@0.16.2:
+  version "0.16.2"
+  resolved "https://registry.yarnpkg.com/send/-/send-0.16.2.tgz#6ecca1e0f8c156d141597559848df64730a6bbc1"
+  dependencies:
+    debug "2.6.9"
+    depd "~1.1.2"
+    destroy "~1.0.4"
+    encodeurl "~1.0.2"
+    escape-html "~1.0.3"
+    etag "~1.8.1"
+    fresh "0.5.2"
+    http-errors "~1.6.2"
+    mime "1.4.1"
+    ms "2.0.0"
+    on-finished "~2.3.0"
+    range-parser "~1.2.0"
+    statuses "~1.4.0"
+
+serialize-javascript@^1.4.0:
+  version "1.4.0"
+  resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-1.4.0.tgz#7c958514db6ac2443a8abc062dc9f7886a7f6005"
+
+serve-static@1.13.2:
+  version "1.13.2"
+  resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.13.2.tgz#095e8472fd5b46237db50ce486a43f4b86c6cec1"
+  dependencies:
+    encodeurl "~1.0.2"
+    escape-html "~1.0.3"
+    parseurl "~1.3.2"
+    send "0.16.2"
+
+set-blocking@^2.0.0, set-blocking@~2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7"
+
+set-immediate-shim@^1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/set-immediate-shim/-/set-immediate-shim-1.0.1.tgz#4b2b1b27eb808a9f8dcc481a58e5e56f599f3f61"
+
+set-value@^0.4.3:
+  version "0.4.3"
+  resolved "https://registry.yarnpkg.com/set-value/-/set-value-0.4.3.tgz#7db08f9d3d22dc7f78e53af3c3bf4666ecdfccf1"
+  dependencies:
+    extend-shallow "^2.0.1"
+    is-extendable "^0.1.1"
+    is-plain-object "^2.0.1"
+    to-object-path "^0.3.0"
+
+set-value@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/set-value/-/set-value-2.0.0.tgz#71ae4a88f0feefbbf52d1ea604f3fb315ebb6274"
+  dependencies:
+    extend-shallow "^2.0.1"
+    is-extendable "^0.1.1"
+    is-plain-object "^2.0.3"
+    split-string "^3.0.1"
+
+setimmediate@^1.0.4:
+  version "1.0.5"
+  resolved "https://registry.yarnpkg.com/setimmediate/-/setimmediate-1.0.5.tgz#290cbb232e306942d7d7ea9b83732ab7856f8285"
+
+setprototypeof@1.0.3:
+  version "1.0.3"
+  resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.0.3.tgz#66567e37043eeb4f04d91bd658c0cbefb55b8e04"
+
+setprototypeof@1.1.0:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.0.tgz#d0bd85536887b6fe7c0d818cb962d9d91c54e656"
+
+sha.js@^2.4.0, sha.js@^2.4.8:
+  version "2.4.11"
+  resolved "https://registry.yarnpkg.com/sha.js/-/sha.js-2.4.11.tgz#37a5cf0b81ecbc6943de109ba2960d1b26584ae7"
+  dependencies:
+    inherits "^2.0.1"
+    safe-buffer "^5.0.1"
+
+shallow-clone@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/shallow-clone/-/shallow-clone-1.0.0.tgz#4480cd06e882ef68b2ad88a3ea54832e2c48b571"
+  dependencies:
+    is-extendable "^0.1.1"
+    kind-of "^5.0.0"
+    mixin-object "^2.0.1"
+
+shebang-command@^1.2.0:
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea"
+  dependencies:
+    shebang-regex "^1.0.0"
+
+shebang-regex@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-1.0.0.tgz#da42f49740c0b42db2ca9728571cb190c98efea3"
+
+shelljs@^0.7.5, shelljs@^0.7.6:
+  version "0.7.8"
+  resolved "https://registry.yarnpkg.com/shelljs/-/shelljs-0.7.8.tgz#decbcf874b0d1e5fb72e14b164a9683048e9acb3"
+  dependencies:
+    glob "^7.0.0"
+    interpret "^1.0.0"
+    rechoir "^0.6.2"
+
+showdown@^1.8.6:
+  version "1.8.6"
+  resolved "https://registry.yarnpkg.com/showdown/-/showdown-1.8.6.tgz#91ea4ee3b7a5448aaca6820a4e27e690c6ad771c"
+  dependencies:
+    yargs "^10.0.3"
+
+shvl@^1.3.0:
+  version "1.3.1"
+  resolved "https://registry.yarnpkg.com/shvl/-/shvl-1.3.1.tgz#6c20a17b4a20b08e9f8cab60c50a92229fcc176e"
+
+sigmund@^1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/sigmund/-/sigmund-1.0.1.tgz#3ff21f198cad2175f9f3b781853fd94d0d19b590"
+
+signal-exit@^3.0.0, signal-exit@^3.0.2:
+  version "3.0.2"
+  resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.2.tgz#b5fdc08f1287ea1178628e415e25132b73646c6d"
+
+sinon-chai@^2.8.0:
+  version "2.14.0"
+  resolved "https://registry.yarnpkg.com/sinon-chai/-/sinon-chai-2.14.0.tgz#da7dd4cc83cd6a260b67cca0f7a9fdae26a1205d"
+
+sinon-stub-promise@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/sinon-stub-promise/-/sinon-stub-promise-4.0.0.tgz#6d498ba1198557cd01e3466af92dc7df725192c2"
+
+sinon@^2.1.0:
+  version "2.4.1"
+  resolved "https://registry.yarnpkg.com/sinon/-/sinon-2.4.1.tgz#021fd64b54cb77d9d2fb0d43cdedfae7629c3a36"
+  dependencies:
+    diff "^3.1.0"
+    formatio "1.2.0"
+    lolex "^1.6.0"
+    native-promise-only "^0.8.1"
+    path-to-regexp "^1.7.0"
+    samsam "^1.1.3"
+    text-encoding "0.6.4"
+    type-detect "^4.0.0"
+
+slash@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/slash/-/slash-1.0.0.tgz#c41f2f6c39fc16d1cd17ad4b5d896114ae470d55"
+
+slice-ansi@0.0.4:
+  version "0.0.4"
+  resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-0.0.4.tgz#edbf8903f66f7ce2f8eafd6ceed65e264c831b35"
+
+smart-buffer@^1.0.13:
+  version "1.1.15"
+  resolved "https://registry.yarnpkg.com/smart-buffer/-/smart-buffer-1.1.15.tgz#7f114b5b65fab3e2a35aa775bb12f0d1c649bf16"
+
+snapdragon-node@^2.0.1:
+  version "2.1.1"
+  resolved "https://registry.yarnpkg.com/snapdragon-node/-/snapdragon-node-2.1.1.tgz#6c175f86ff14bdb0724563e8f3c1b021a286853b"
+  dependencies:
+    define-property "^1.0.0"
+    isobject "^3.0.0"
+    snapdragon-util "^3.0.1"
+
+snapdragon-util@^3.0.1:
+  version "3.0.1"
+  resolved "https://registry.yarnpkg.com/snapdragon-util/-/snapdragon-util-3.0.1.tgz#f956479486f2acd79700693f6f7b805e45ab56e2"
+  dependencies:
+    kind-of "^3.2.0"
+
+snapdragon@^0.8.1:
+  version "0.8.2"
+  resolved "https://registry.yarnpkg.com/snapdragon/-/snapdragon-0.8.2.tgz#64922e7c565b0e14204ba1aa7d6964278d25182d"
+  dependencies:
+    base "^0.11.1"
+    debug "^2.2.0"
+    define-property "^0.2.5"
+    extend-shallow "^2.0.1"
+    map-cache "^0.2.2"
+    source-map "^0.5.6"
+    source-map-resolve "^0.5.0"
+    use "^3.1.0"
+
+sntp@1.x.x:
+  version "1.0.9"
+  resolved "https://registry.yarnpkg.com/sntp/-/sntp-1.0.9.tgz#6541184cc90aeea6c6e7b35e2659082443c66198"
+  dependencies:
+    hoek "2.x.x"
+
+sntp@2.x.x:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/sntp/-/sntp-2.1.0.tgz#2c6cec14fedc2222739caf9b5c3d85d1cc5a2cc8"
+  dependencies:
+    hoek "4.x.x"
+
+socket.io-adapter@0.5.0:
+  version "0.5.0"
+  resolved "https://registry.yarnpkg.com/socket.io-adapter/-/socket.io-adapter-0.5.0.tgz#cb6d4bb8bec81e1078b99677f9ced0046066bb8b"
+  dependencies:
+    debug "2.3.3"
+    socket.io-parser "2.3.1"
+
+socket.io-client@1.7.3:
+  version "1.7.3"
+  resolved "https://registry.yarnpkg.com/socket.io-client/-/socket.io-client-1.7.3.tgz#b30e86aa10d5ef3546601c09cde4765e381da377"
+  dependencies:
+    backo2 "1.0.2"
+    component-bind "1.0.0"
+    component-emitter "1.2.1"
+    debug "2.3.3"
+    engine.io-client "1.8.3"
+    has-binary "0.1.7"
+    indexof "0.0.1"
+    object-component "0.0.3"
+    parseuri "0.0.5"
+    socket.io-parser "2.3.1"
+    to-array "0.1.4"
+
+socket.io-parser@2.3.1:
+  version "2.3.1"
+  resolved "https://registry.yarnpkg.com/socket.io-parser/-/socket.io-parser-2.3.1.tgz#dd532025103ce429697326befd64005fcfe5b4a0"
+  dependencies:
+    component-emitter "1.1.2"
+    debug "2.2.0"
+    isarray "0.0.1"
+    json3 "3.3.2"
+
+socket.io@1.7.3:
+  version "1.7.3"
+  resolved "https://registry.yarnpkg.com/socket.io/-/socket.io-1.7.3.tgz#b8af9caba00949e568e369f1327ea9be9ea2461b"
+  dependencies:
+    debug "2.3.3"
+    engine.io "1.8.3"
+    has-binary "0.1.7"
+    object-assign "4.1.0"
+    socket.io-adapter "0.5.0"
+    socket.io-client "1.7.3"
+    socket.io-parser "2.3.1"
+
+socks-proxy-agent@2:
+  version "2.1.1"
+  resolved "https://registry.yarnpkg.com/socks-proxy-agent/-/socks-proxy-agent-2.1.1.tgz#86ebb07193258637870e13b7bd99f26c663df3d3"
+  dependencies:
+    agent-base "2"
+    extend "3"
+    socks "~1.1.5"
+
+socks@~1.1.5:
+  version "1.1.10"
+  resolved "https://registry.yarnpkg.com/socks/-/socks-1.1.10.tgz#5b8b7fc7c8f341c53ed056e929b7bf4de8ba7b5a"
+  dependencies:
+    ip "^1.1.4"
+    smart-buffer "^1.0.13"
+
+sort-keys@^1.0.0:
+  version "1.1.2"
+  resolved "https://registry.yarnpkg.com/sort-keys/-/sort-keys-1.1.2.tgz#441b6d4d346798f1b4e49e8920adfba0e543f9ad"
+  dependencies:
+    is-plain-obj "^1.0.0"
+
+sortablejs@^1.7.0:
+  version "1.7.0"
+  resolved "https://registry.yarnpkg.com/sortablejs/-/sortablejs-1.7.0.tgz#80a2b2370abd568e1cec8c271131ef30a904fa28"
+
+source-list-map@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/source-list-map/-/source-list-map-2.0.0.tgz#aaa47403f7b245a92fbc97ea08f250d6087ed085"
+
+source-list-map@~0.1.7:
+  version "0.1.8"
+  resolved "https://registry.yarnpkg.com/source-list-map/-/source-list-map-0.1.8.tgz#c550b2ab5427f6b3f21f5afead88c4f5587b2106"
+
+source-map-resolve@^0.5.0:
+  version "0.5.1"
+  resolved "https://registry.yarnpkg.com/source-map-resolve/-/source-map-resolve-0.5.1.tgz#7ad0f593f2281598e854df80f19aae4b92d7a11a"
+  dependencies:
+    atob "^2.0.0"
+    decode-uri-component "^0.2.0"
+    resolve-url "^0.2.1"
+    source-map-url "^0.4.0"
+    urix "^0.1.0"
+
+source-map-support@^0.4.15:
+  version "0.4.18"
+  resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.4.18.tgz#0286a6de8be42641338594e97ccea75f0a2c585f"
+  dependencies:
+    source-map "^0.5.6"
+
+source-map-url@^0.4.0:
+  version "0.4.0"
+  resolved "https://registry.yarnpkg.com/source-map-url/-/source-map-url-0.4.0.tgz#3e935d7ddd73631b97659956d55128e87b5084a3"
+
+source-map@0.5.x, source-map@^0.5.1, source-map@^0.5.3, source-map@^0.5.6, source-map@^0.5.7, source-map@~0.5.1, source-map@~0.5.3:
+  version "0.5.7"
+  resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc"
+
+source-map@^0.4.2, source-map@^0.4.4:
+  version "0.4.4"
+  resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.4.4.tgz#eba4f5da9c0dc999de68032d8b4f76173652036b"
+  dependencies:
+    amdefine ">=0.0.4"
+
+source-map@^0.6.1, source-map@~0.6.1:
+  version "0.6.1"
+  resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263"
+
+source-map@~0.2.0:
+  version "0.2.0"
+  resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.2.0.tgz#dab73fbcfc2ba819b4de03bd6f6eaa48164b3f9d"
+  dependencies:
+    amdefine ">=0.0.4"
+
+spdx-correct@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-3.0.0.tgz#05a5b4d7153a195bc92c3c425b69f3b2a9524c82"
+  dependencies:
+    spdx-expression-parse "^3.0.0"
+    spdx-license-ids "^3.0.0"
+
+spdx-exceptions@^2.1.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/spdx-exceptions/-/spdx-exceptions-2.1.0.tgz#2c7ae61056c714a5b9b9b2b2af7d311ef5c78fe9"
+
+spdx-expression-parse@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/spdx-expression-parse/-/spdx-expression-parse-3.0.0.tgz#99e119b7a5da00e05491c9fa338b7904823b41d0"
+  dependencies:
+    spdx-exceptions "^2.1.0"
+    spdx-license-ids "^3.0.0"
+
+spdx-license-ids@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.0.tgz#7a7cd28470cc6d3a1cfe6d66886f6bc430d3ac87"
+
+split-string@^3.0.1, split-string@^3.0.2:
+  version "3.1.0"
+  resolved "https://registry.yarnpkg.com/split-string/-/split-string-3.1.0.tgz#7cb09dda3a86585705c64b39a6466038682e8fe2"
+  dependencies:
+    extend-shallow "^3.0.0"
+
+sprintf-js@~1.0.2:
+  version "1.0.3"
+  resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c"
+
+sshpk@^1.7.0:
+  version "1.14.1"
+  resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.14.1.tgz#130f5975eddad963f1d56f92b9ac6c51fa9f83eb"
+  dependencies:
+    asn1 "~0.2.3"
+    assert-plus "^1.0.0"
+    dashdash "^1.12.0"
+    getpass "^0.1.1"
+  optionalDependencies:
+    bcrypt-pbkdf "^1.0.0"
+    ecc-jsbn "~0.1.1"
+    jsbn "~0.1.0"
+    tweetnacl "~0.14.0"
+
+ssri@^5.2.4:
+  version "5.3.0"
+  resolved "https://registry.yarnpkg.com/ssri/-/ssri-5.3.0.tgz#ba3872c9c6d33a0704a7d71ff045e5ec48999d06"
+  dependencies:
+    safe-buffer "^5.1.1"
+
+stackframe@^1.0.3:
+  version "1.0.4"
+  resolved "https://registry.yarnpkg.com/stackframe/-/stackframe-1.0.4.tgz#357b24a992f9427cba6b545d96a14ed2cbca187b"
+
+static-extend@^0.1.1:
+  version "0.1.2"
+  resolved "https://registry.yarnpkg.com/static-extend/-/static-extend-0.1.2.tgz#60809c39cbff55337226fd5e0b520f341f1fb5c6"
+  dependencies:
+    define-property "^0.2.5"
+    object-copy "^0.1.0"
+
+"statuses@>= 1.3.1 < 2", "statuses@>= 1.4.0 < 2":
+  version "1.5.0"
+  resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c"
+
+statuses@~1.3.1:
+  version "1.3.1"
+  resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.3.1.tgz#faf51b9eb74aaef3b3acf4ad5f61abf24cb7b93e"
+
+statuses@~1.4.0:
+  version "1.4.0"
+  resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.4.0.tgz#bb73d446da2796106efcc1b601a253d6c46bd087"
+
+stdout-stream@^1.4.0:
+  version "1.4.0"
+  resolved "https://registry.yarnpkg.com/stdout-stream/-/stdout-stream-1.4.0.tgz#a2c7c8587e54d9427ea9edb3ac3f2cd522df378b"
+  dependencies:
+    readable-stream "^2.0.1"
+
+stream-browserify@^2.0.1:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/stream-browserify/-/stream-browserify-2.0.1.tgz#66266ee5f9bdb9940a4e4514cafb43bb71e5c9db"
+  dependencies:
+    inherits "~2.0.1"
+    readable-stream "^2.0.2"
+
+stream-each@^1.1.0:
+  version "1.2.2"
+  resolved "https://registry.yarnpkg.com/stream-each/-/stream-each-1.2.2.tgz#8e8c463f91da8991778765873fe4d960d8f616bd"
+  dependencies:
+    end-of-stream "^1.1.0"
+    stream-shift "^1.0.0"
+
+stream-http@^2.7.2:
+  version "2.8.1"
+  resolved "https://registry.yarnpkg.com/stream-http/-/stream-http-2.8.1.tgz#d0441be1a457a73a733a8a7b53570bebd9ef66a4"
+  dependencies:
+    builtin-status-codes "^3.0.0"
+    inherits "^2.0.1"
+    readable-stream "^2.3.3"
+    to-arraybuffer "^1.0.0"
+    xtend "^4.0.0"
+
+stream-shift@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/stream-shift/-/stream-shift-1.0.0.tgz#d5c752825e5367e786f78e18e445ea223a155952"
+
+strict-uri-encode@^1.0.0:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz#279b225df1d582b1f54e65addd4352e18faa0713"
+
+string-width@^1.0.1, string-width@^1.0.2:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/string-width/-/string-width-1.0.2.tgz#118bdf5b8cdc51a2a7e70d211e07e2b0b9b107d3"
+  dependencies:
+    code-point-at "^1.0.0"
+    is-fullwidth-code-point "^1.0.0"
+    strip-ansi "^3.0.0"
+
+string-width@^2.0.0, string-width@^2.1.1:
+  version "2.1.1"
+  resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e"
+  dependencies:
+    is-fullwidth-code-point "^2.0.0"
+    strip-ansi "^4.0.0"
+
+string_decoder@^1.0.0, string_decoder@~1.1.1:
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8"
+  dependencies:
+    safe-buffer "~5.1.0"
+
+string_decoder@~0.10.x:
+  version "0.10.31"
+  resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-0.10.31.tgz#62e203bc41766c6c28c9fc84301dab1c5310fa94"
+
+stringstream@~0.0.4, stringstream@~0.0.5:
+  version "0.0.5"
+  resolved "https://registry.yarnpkg.com/stringstream/-/stringstream-0.0.5.tgz#4e484cd4de5a0bbbee18e46307710a8a81621878"
+
+strip-ansi@^3.0.0, strip-ansi@^3.0.1:
+  version "3.0.1"
+  resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf"
+  dependencies:
+    ansi-regex "^2.0.0"
+
+strip-ansi@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-4.0.0.tgz#a8479022eb1ac368a871389b635262c505ee368f"
+  dependencies:
+    ansi-regex "^3.0.0"
+
+strip-bom@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-2.0.0.tgz#6219a85616520491f35788bdbf1447a99c7e6b0e"
+  dependencies:
+    is-utf8 "^0.2.0"
+
+strip-bom@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3"
+
+strip-eof@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/strip-eof/-/strip-eof-1.0.0.tgz#bb43ff5598a6eb05d89b59fcd129c983313606bf"
+
+strip-indent@^1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/strip-indent/-/strip-indent-1.0.1.tgz#0c7962a6adefa7bbd4ac366460a638552ae1a0a2"
+  dependencies:
+    get-stdin "^4.0.1"
+
+strip-json-comments@~2.0.1:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a"
+
+supports-color@3.1.2:
+  version "3.1.2"
+  resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-3.1.2.tgz#72a262894d9d408b956ca05ff37b2ed8a6e2a2d5"
+  dependencies:
+    has-flag "^1.0.0"
+
+supports-color@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7"
+
+supports-color@^3.1.0, supports-color@^3.2.3:
+  version "3.2.3"
+  resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-3.2.3.tgz#65ac0504b3954171d8a64946b2ae3cbb8a5f54f6"
+  dependencies:
+    has-flag "^1.0.0"
+
+supports-color@^4.2.1:
+  version "4.5.0"
+  resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-4.5.0.tgz#be7a0de484dec5c5cddf8b3d59125044912f635b"
+  dependencies:
+    has-flag "^2.0.0"
+
+supports-color@^5.3.0:
+  version "5.3.0"
+  resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.3.0.tgz#5b24ac15db80fa927cf5227a4a33fd3c4c7676c0"
+  dependencies:
+    has-flag "^3.0.0"
+
+svgo@^0.7.0:
+  version "0.7.2"
+  resolved "https://registry.yarnpkg.com/svgo/-/svgo-0.7.2.tgz#9f5772413952135c6fefbf40afe6a4faa88b4bb5"
+  dependencies:
+    coa "~1.0.1"
+    colors "~1.1.2"
+    csso "~2.3.1"
+    js-yaml "~3.7.0"
+    mkdirp "~0.5.1"
+    sax "~1.2.1"
+    whet.extend "~0.9.9"
+
+table@^3.7.8:
+  version "3.8.3"
+  resolved "https://registry.yarnpkg.com/table/-/table-3.8.3.tgz#2bbc542f0fda9861a755d3947fefd8b3f513855f"
+  dependencies:
+    ajv "^4.7.0"
+    ajv-keywords "^1.0.0"
+    chalk "^1.1.1"
+    lodash "^4.0.0"
+    slice-ansi "0.0.4"
+    string-width "^2.0.0"
+
+tapable@^0.2.7:
+  version "0.2.8"
+  resolved "https://registry.yarnpkg.com/tapable/-/tapable-0.2.8.tgz#99372a5c999bf2df160afc0d74bed4f47948cd22"
+
+tar-pack@^3.4.0:
+  version "3.4.1"
+  resolved "https://registry.yarnpkg.com/tar-pack/-/tar-pack-3.4.1.tgz#e1dbc03a9b9d3ba07e896ad027317eb679a10a1f"
+  dependencies:
+    debug "^2.2.0"
+    fstream "^1.0.10"
+    fstream-ignore "^1.0.5"
+    once "^1.3.3"
+    readable-stream "^2.1.4"
+    rimraf "^2.5.1"
+    tar "^2.2.1"
+    uid-number "^0.0.6"
+
+tar@^2.0.0, tar@^2.2.1:
+  version "2.2.1"
+  resolved "https://registry.yarnpkg.com/tar/-/tar-2.2.1.tgz#8e4d2a256c0e2185c6b18ad694aec968b83cb1d1"
+  dependencies:
+    block-stream "*"
+    fstream "^1.0.2"
+    inherits "2"
+
+test-exclude@^4.2.1:
+  version "4.2.1"
+  resolved "https://registry.yarnpkg.com/test-exclude/-/test-exclude-4.2.1.tgz#dfa222f03480bca69207ca728b37d74b45f724fa"
+  dependencies:
+    arrify "^1.0.1"
+    micromatch "^3.1.8"
+    object-assign "^4.1.0"
+    read-pkg-up "^1.0.1"
+    require-main-filename "^1.0.1"
+
+text-encoding@0.6.4:
+  version "0.6.4"
+  resolved "https://registry.yarnpkg.com/text-encoding/-/text-encoding-0.6.4.tgz#e399a982257a276dae428bb92845cb71bdc26d19"
+
+text-table@^0.2.0, text-table@~0.2.0:
+  version "0.2.0"
+  resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4"
+
+throttleit@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/throttleit/-/throttleit-1.0.0.tgz#9e785836daf46743145a5984b6268d828528ac6c"
+
+through2@^2.0.0:
+  version "2.0.3"
+  resolved "https://registry.yarnpkg.com/through2/-/through2-2.0.3.tgz#0004569b37c7c74ba39c43f3ced78d1ad94140be"
+  dependencies:
+    readable-stream "^2.1.5"
+    xtend "~4.0.1"
+
+through@^2.3.6:
+  version "2.3.8"
+  resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5"
+
+thunkify@~2.1.1:
+  version "2.1.2"
+  resolved "https://registry.yarnpkg.com/thunkify/-/thunkify-2.1.2.tgz#faa0e9d230c51acc95ca13a361ac05ca7e04553d"
+
+time-stamp@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/time-stamp/-/time-stamp-2.0.0.tgz#95c6a44530e15ba8d6f4a3ecb8c3a3fac46da357"
+
+timers-browserify@^2.0.4:
+  version "2.0.6"
+  resolved "https://registry.yarnpkg.com/timers-browserify/-/timers-browserify-2.0.6.tgz#241e76927d9ca05f4d959819022f5b3664b64bae"
+  dependencies:
+    setimmediate "^1.0.4"
+
+tmp@0.0.31:
+  version "0.0.31"
+  resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.31.tgz#8f38ab9438e17315e5dbd8b3657e8bfb277ae4a7"
+  dependencies:
+    os-tmpdir "~1.0.1"
+
+tmp@0.0.x:
+  version "0.0.33"
+  resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9"
+  dependencies:
+    os-tmpdir "~1.0.2"
+
+to-array@0.1.4:
+  version "0.1.4"
+  resolved "https://registry.yarnpkg.com/to-array/-/to-array-0.1.4.tgz#17e6c11f73dd4f3d74cda7a4ff3238e9ad9bf890"
+
+to-arraybuffer@^1.0.0:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz#7d229b1fcc637e466ca081180836a7aabff83f43"
+
+to-fast-properties@^1.0.3:
+  version "1.0.3"
+  resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-1.0.3.tgz#b83571fa4d8c25b82e231b06e3a3055de4ca1a47"
+
+to-object-path@^0.3.0:
+  version "0.3.0"
+  resolved "https://registry.yarnpkg.com/to-object-path/-/to-object-path-0.3.0.tgz#297588b7b0e7e0ac08e04e672f85c1f4999e17af"
+  dependencies:
+    kind-of "^3.0.2"
+
+to-regex-range@^2.1.0:
+  version "2.1.1"
+  resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-2.1.1.tgz#7c80c17b9dfebe599e27367e0d4dd5590141db38"
+  dependencies:
+    is-number "^3.0.0"
+    repeat-string "^1.6.1"
+
+to-regex@^3.0.1, to-regex@^3.0.2:
+  version "3.0.2"
+  resolved "https://registry.yarnpkg.com/to-regex/-/to-regex-3.0.2.tgz#13cfdd9b336552f30b51f33a8ae1b42a7a7599ce"
+  dependencies:
+    define-property "^2.0.2"
+    extend-shallow "^3.0.2"
+    regex-not "^1.0.2"
+    safe-regex "^1.1.0"
+
+toposort@^1.0.0:
+  version "1.0.6"
+  resolved "https://registry.yarnpkg.com/toposort/-/toposort-1.0.6.tgz#c31748e55d210effc00fdcdc7d6e68d7d7bb9cec"
+
+tough-cookie@~2.3.0, tough-cookie@~2.3.3:
+  version "2.3.4"
+  resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.3.4.tgz#ec60cee38ac675063ffc97a5c18970578ee83655"
+  dependencies:
+    punycode "^1.4.1"
+
+trim-newlines@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-1.0.0.tgz#5887966bb582a4503a41eb524f7d35011815a613"
+
+trim-right@^1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/trim-right/-/trim-right-1.0.1.tgz#cb2e1203067e0c8de1f614094b9fe45704ea6003"
+
+"true-case-path@^1.0.2":
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/true-case-path/-/true-case-path-1.0.2.tgz#7ec91130924766c7f573be3020c34f8fdfd00d62"
+  dependencies:
+    glob "^6.0.4"
+
+tryer@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/tryer/-/tryer-1.0.0.tgz#027b69fa823225e551cace3ef03b11f6ab37c1d7"
+
+tty-browserify@0.0.0:
+  version "0.0.0"
+  resolved "https://registry.yarnpkg.com/tty-browserify/-/tty-browserify-0.0.0.tgz#a157ba402da24e9bf957f9aa69d524eed42901a6"
+
+tunnel-agent@^0.6.0:
+  version "0.6.0"
+  resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz#27a5dea06b36b04a0a9966774b290868f0fc40fd"
+  dependencies:
+    safe-buffer "^5.0.1"
+
+tunnel-agent@~0.4.1:
+  version "0.4.3"
+  resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.4.3.tgz#6373db76909fe570e08d73583365ed828a74eeeb"
+
+tweetnacl@^0.14.3, tweetnacl@~0.14.0:
+  version "0.14.5"
+  resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64"
+
+type-check@~0.3.2:
+  version "0.3.2"
+  resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.3.2.tgz#5884cab512cf1d355e3fb784f30804b2b520db72"
+  dependencies:
+    prelude-ls "~1.1.2"
+
+type-detect@0.1.1:
+  version "0.1.1"
+  resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-0.1.1.tgz#0ba5ec2a885640e470ea4e8505971900dac58822"
+
+type-detect@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-1.0.0.tgz#762217cc06db258ec48908a1298e8b95121e8ea2"
+
+type-detect@^4.0.0:
+  version "4.0.8"
+  resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.8.tgz#7646fb5f18871cfbb7749e69bd39a6388eb7450c"
+
+type-is@~1.6.15, type-is@~1.6.16:
+  version "1.6.16"
+  resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.16.tgz#f89ce341541c672b25ee7ae3c73dee3b2be50194"
+  dependencies:
+    media-typer "0.3.0"
+    mime-types "~2.1.18"
+
+typedarray@^0.0.6:
+  version "0.0.6"
+  resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777"
+
+uglify-js@3.3.x:
+  version "3.3.20"
+  resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.3.20.tgz#dc8bdee7d454c7d31dddc36f922d170bfcee3a0a"
+  dependencies:
+    commander "~2.15.0"
+    source-map "~0.6.1"
+
+uglify-js@^2.6, uglify-js@^2.8.29:
+  version "2.8.29"
+  resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-2.8.29.tgz#29c5733148057bb4e1f75df35b7a9cb72e6a59dd"
+  dependencies:
+    source-map "~0.5.1"
+    yargs "~3.10.0"
+  optionalDependencies:
+    uglify-to-browserify "~1.0.0"
+
+uglify-to-browserify@~1.0.0:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/uglify-to-browserify/-/uglify-to-browserify-1.0.2.tgz#6e0924d6bda6b5afe349e39a6d632850a0f882b7"
+
+uglifyjs-webpack-plugin@^0.4.6:
+  version "0.4.6"
+  resolved "https://registry.yarnpkg.com/uglifyjs-webpack-plugin/-/uglifyjs-webpack-plugin-0.4.6.tgz#b951f4abb6bd617e66f63eb891498e391763e309"
+  dependencies:
+    source-map "^0.5.6"
+    uglify-js "^2.8.29"
+    webpack-sources "^1.0.1"
+
+uid-number@^0.0.6:
+  version "0.0.6"
+  resolved "https://registry.yarnpkg.com/uid-number/-/uid-number-0.0.6.tgz#0ea10e8035e8eb5b8e4449f06da1c730663baa81"
+
+ultron@1.0.x:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/ultron/-/ultron-1.0.2.tgz#ace116ab557cd197386a4e88f4685378c8b2e4fa"
+
+underscore@^1.8.3:
+  version "1.8.3"
+  resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.8.3.tgz#4f3fb53b106e6097fcf9cb4109f2a5e9bdfa5022"
+
+union-value@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/union-value/-/union-value-1.0.0.tgz#5c71c34cb5bad5dcebe3ea0cd08207ba5aa1aea4"
+  dependencies:
+    arr-union "^3.1.0"
+    get-value "^2.0.6"
+    is-extendable "^0.1.1"
+    set-value "^0.4.3"
+
+uniq@^1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/uniq/-/uniq-1.0.1.tgz#b31c5ae8254844a3a8281541ce2b04b865a734ff"
+
+uniqid@^4.0.0:
+  version "4.1.1"
+  resolved "https://registry.yarnpkg.com/uniqid/-/uniqid-4.1.1.tgz#89220ddf6b751ae52b5f72484863528596bb84c1"
+  dependencies:
+    macaddress "^0.2.8"
+
+uniqs@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/uniqs/-/uniqs-2.0.0.tgz#ffede4b36b25290696e6e165d4a59edb998e6b02"
+
+unique-filename@^1.1.0:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/unique-filename/-/unique-filename-1.1.0.tgz#d05f2fe4032560871f30e93cbe735eea201514f3"
+  dependencies:
+    unique-slug "^2.0.0"
+
+unique-slug@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/unique-slug/-/unique-slug-2.0.0.tgz#db6676e7c7cc0629878ff196097c78855ae9f4ab"
+  dependencies:
+    imurmurhash "^0.1.4"
+
+unpipe@1.0.0, unpipe@~1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec"
+
+unset-value@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/unset-value/-/unset-value-1.0.0.tgz#8376873f7d2335179ffb1e6fc3a8ed0dfc8ab559"
+  dependencies:
+    has-value "^0.3.1"
+    isobject "^3.0.0"
+
+upath@^1.0.0:
+  version "1.0.4"
+  resolved "https://registry.yarnpkg.com/upath/-/upath-1.0.4.tgz#ee2321ba0a786c50973db043a50b7bcba822361d"
+
+upper-case@^1.1.1:
+  version "1.1.3"
+  resolved "https://registry.yarnpkg.com/upper-case/-/upper-case-1.1.3.tgz#f6b4501c2ec4cdd26ba78be7222961de77621598"
+
+uri-js@^3.0.2:
+  version "3.0.2"
+  resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-3.0.2.tgz#f90b858507f81dea4dcfbb3c4c3dbfa2b557faaa"
+  dependencies:
+    punycode "^2.1.0"
+
+urix@^0.1.0:
+  version "0.1.0"
+  resolved "https://registry.yarnpkg.com/urix/-/urix-0.1.0.tgz#da937f7a62e21fec1fd18d49b35c2935067a6c72"
+
+url-loader@^0.5.8:
+  version "0.5.9"
+  resolved "https://registry.yarnpkg.com/url-loader/-/url-loader-0.5.9.tgz#cc8fea82c7b906e7777019250869e569e995c295"
+  dependencies:
+    loader-utils "^1.0.2"
+    mime "1.3.x"
+
+url@^0.11.0:
+  version "0.11.0"
+  resolved "https://registry.yarnpkg.com/url/-/url-0.11.0.tgz#3838e97cfc60521eb73c525a8e55bfdd9e2e28f1"
+  dependencies:
+    punycode "1.3.2"
+    querystring "0.2.0"
+
+use@^3.1.0:
+  version "3.1.0"
+  resolved "https://registry.yarnpkg.com/use/-/use-3.1.0.tgz#14716bf03fdfefd03040aef58d8b4b85f3a7c544"
+  dependencies:
+    kind-of "^6.0.2"
+
+user-home@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/user-home/-/user-home-2.0.0.tgz#9c70bfd8169bc1dcbf48604e0f04b8b49cde9e9f"
+  dependencies:
+    os-homedir "^1.0.0"
+
+useragent@^2.1.12:
+  version "2.3.0"
+  resolved "https://registry.yarnpkg.com/useragent/-/useragent-2.3.0.tgz#217f943ad540cb2128658ab23fc960f6a88c9972"
+  dependencies:
+    lru-cache "4.1.x"
+    tmp "0.0.x"
+
+util-deprecate@~1.0.1:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
+
+util@0.10.3, util@^0.10.3:
+  version "0.10.3"
+  resolved "https://registry.yarnpkg.com/util/-/util-0.10.3.tgz#7afb1afe50805246489e3db7fe0ed379336ac0f9"
+  dependencies:
+    inherits "2.0.1"
+
+utila@~0.3:
+  version "0.3.3"
+  resolved "https://registry.yarnpkg.com/utila/-/utila-0.3.3.tgz#d7e8e7d7e309107092b05f8d9688824d633a4226"
+
+utila@~0.4:
+  version "0.4.0"
+  resolved "https://registry.yarnpkg.com/utila/-/utila-0.4.0.tgz#8a16a05d445657a3aea5eecc5b12a4fa5379772c"
+
+utils-merge@1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713"
+
+uuid@^3.0.0, uuid@^3.1.0:
+  version "3.2.1"
+  resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.2.1.tgz#12c528bb9d58d0b9265d9a2f6f0fe8be17ff1f14"
+
+validate-npm-package-license@^3.0.1:
+  version "3.0.3"
+  resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.3.tgz#81643bcbef1bdfecd4623793dc4648948ba98338"
+  dependencies:
+    spdx-correct "^3.0.0"
+    spdx-expression-parse "^3.0.0"
+
+vary@~1.1.2:
+  version "1.1.2"
+  resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc"
+
+vendors@^1.0.0:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/vendors/-/vendors-1.0.1.tgz#37ad73c8ee417fb3d580e785312307d274847f22"
+
+verror@1.10.0:
+  version "1.10.0"
+  resolved "https://registry.yarnpkg.com/verror/-/verror-1.10.0.tgz#3a105ca17053af55d6e270c1f8288682e18da400"
+  dependencies:
+    assert-plus "^1.0.0"
+    core-util-is "1.0.2"
+    extsprintf "^1.2.0"
+
+vm-browserify@0.0.4:
+  version "0.0.4"
+  resolved "https://registry.yarnpkg.com/vm-browserify/-/vm-browserify-0.0.4.tgz#5d7ea45bbef9e4a6ff65f95438e0a87c357d5a73"
+  dependencies:
+    indexof "0.0.1"
+
+void-elements@^2.0.0:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/void-elements/-/void-elements-2.0.1.tgz#c066afb582bb1cb4128d60ea92392e94d5e9dbec"
+
+vue-hot-reload-api@^2.1.0:
+  version "2.3.0"
+  resolved "https://registry.yarnpkg.com/vue-hot-reload-api/-/vue-hot-reload-api-2.3.0.tgz#97976142405d13d8efae154749e88c4e358cf926"
+
+vue-i18n-xgettext@^0.0.4:
+  version "0.0.4"
+  resolved "https://registry.yarnpkg.com/vue-i18n-xgettext/-/vue-i18n-xgettext-0.0.4.tgz#80ad654e65fb33bb5fcbd96f338f55605ab1a06f"
+  dependencies:
+    cheerio "^0.22.0"
+    fs "0.0.1-security"
+    minimist "^1.2.0"
+    pofile "^1.0.2"
+
+vue-lazyload@^1.1.4:
+  version "1.2.2"
+  resolved "https://registry.yarnpkg.com/vue-lazyload/-/vue-lazyload-1.2.2.tgz#73335ed32db25264f5957df1a21d277823423743"
+
+vue-loader@^12.1.0:
+  version "12.2.2"
+  resolved "https://registry.yarnpkg.com/vue-loader/-/vue-loader-12.2.2.tgz#2b3a764f27018f975bc78cb8b1f55137548ee2d7"
+  dependencies:
+    consolidate "^0.14.0"
+    hash-sum "^1.0.2"
+    js-beautify "^1.6.3"
+    loader-utils "^1.1.0"
+    lru-cache "^4.0.1"
+    postcss "^5.0.21"
+    postcss-load-config "^1.1.0"
+    postcss-selector-parser "^2.0.0"
+    resolve "^1.3.3"
+    source-map "^0.5.6"
+    vue-hot-reload-api "^2.1.0"
+    vue-style-loader "^3.0.0"
+    vue-template-es2015-compiler "^1.2.2"
+
+vue-masonry@^0.10.16:
+  version "0.10.17"
+  resolved "https://registry.yarnpkg.com/vue-masonry/-/vue-masonry-0.10.17.tgz#39ae7f412ff8492a713b39ad2b3d41c34fb76b91"
+  dependencies:
+    imagesloaded latest
+    masonry latest
+    masonry-layout "4.1.1"
+    vue "^2.0.0"
+
+vue-router@^2.3.1:
+  version "2.8.1"
+  resolved "https://registry.yarnpkg.com/vue-router/-/vue-router-2.8.1.tgz#9833c9ee57ac83beb0269056fefee71713f20695"
+
+vue-style-loader@^3.0.0, vue-style-loader@^3.0.1:
+  version "3.1.2"
+  resolved "https://registry.yarnpkg.com/vue-style-loader/-/vue-style-loader-3.1.2.tgz#6b66ad34998fc9520c2f1e4d5fa4091641c1597a"
+  dependencies:
+    hash-sum "^1.0.2"
+    loader-utils "^1.0.2"
+
+vue-template-compiler@^2.3.3:
+  version "2.5.16"
+  resolved "https://registry.yarnpkg.com/vue-template-compiler/-/vue-template-compiler-2.5.16.tgz#93b48570e56c720cdf3f051cc15287c26fbd04cb"
+  dependencies:
+    de-indent "^1.0.2"
+    he "^1.1.0"
+
+vue-template-es2015-compiler@^1.2.2:
+  version "1.6.0"
+  resolved "https://registry.yarnpkg.com/vue-template-es2015-compiler/-/vue-template-es2015-compiler-1.6.0.tgz#dc42697133302ce3017524356a6c61b7b69b4a18"
+
+vue-upload-component@^2.7.4:
+  version "2.8.5"
+  resolved "https://registry.yarnpkg.com/vue-upload-component/-/vue-upload-component-2.8.5.tgz#680de2934f55c6a38da7b382d0f37fd79995dd42"
+
+vue@^2.0.0, vue@^2.3.3:
+  version "2.5.16"
+  resolved "https://registry.yarnpkg.com/vue/-/vue-2.5.16.tgz#07edb75e8412aaeed871ebafa99f4672584a0085"
+
+vuedraggable@^2.14.1:
+  version "2.16.0"
+  resolved "https://registry.yarnpkg.com/vuedraggable/-/vuedraggable-2.16.0.tgz#52127081a2adb3de5fabd214d404ff3eee63575a"
+  dependencies:
+    sortablejs "^1.7.0"
+
+vuex-persistedstate@^2.5.2:
+  version "2.5.2"
+  resolved "https://registry.yarnpkg.com/vuex-persistedstate/-/vuex-persistedstate-2.5.2.tgz#751752f03bdf6d7fc855106f0666f2ce13b415b5"
+  dependencies:
+    deepmerge "^2.1.0"
+    shvl "^1.3.0"
+
+vuex-router-sync@^5.0.0:
+  version "5.0.0"
+  resolved "https://registry.yarnpkg.com/vuex-router-sync/-/vuex-router-sync-5.0.0.tgz#1a225c17a1dd9e2f74af0a1b2c62072e9492b305"
+
+vuex@^3.0.1:
+  version "3.0.1"
+  resolved "https://registry.yarnpkg.com/vuex/-/vuex-3.0.1.tgz#e761352ebe0af537d4bb755a9b9dc4be3df7efd2"
+
+watchpack@^1.4.0:
+  version "1.5.0"
+  resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-1.5.0.tgz#231e783af830a22f8966f65c4c4bacc814072eed"
+  dependencies:
+    chokidar "^2.0.2"
+    graceful-fs "^4.1.2"
+    neo-async "^2.5.0"
+
+webpack-bundle-analyzer@^2.2.1:
+  version "2.11.1"
+  resolved "https://registry.yarnpkg.com/webpack-bundle-analyzer/-/webpack-bundle-analyzer-2.11.1.tgz#b9fbfb6a32c0a8c1c3237223e90890796b950ab9"
+  dependencies:
+    acorn "^5.3.0"
+    bfj-node4 "^5.2.0"
+    chalk "^2.3.0"
+    commander "^2.13.0"
+    ejs "^2.5.7"
+    express "^4.16.2"
+    filesize "^3.5.11"
+    gzip-size "^4.1.0"
+    lodash "^4.17.4"
+    mkdirp "^0.5.1"
+    opener "^1.4.3"
+    ws "^4.0.0"
+
+webpack-dev-middleware@^1.10.0, webpack-dev-middleware@^1.12.0:
+  version "1.12.2"
+  resolved "https://registry.yarnpkg.com/webpack-dev-middleware/-/webpack-dev-middleware-1.12.2.tgz#f8fc1120ce3b4fc5680ceecb43d777966b21105e"
+  dependencies:
+    memory-fs "~0.4.1"
+    mime "^1.5.0"
+    path-is-absolute "^1.0.0"
+    range-parser "^1.0.3"
+    time-stamp "^2.0.0"
+
+webpack-hot-middleware@^2.18.0:
+  version "2.22.0"
+  resolved "https://registry.yarnpkg.com/webpack-hot-middleware/-/webpack-hot-middleware-2.22.0.tgz#db58c9dd2bd78e7f3868cccb42a20d24b00a7ade"
+  dependencies:
+    ansi-html "0.0.7"
+    html-entities "^1.2.0"
+    querystring "^0.2.0"
+    strip-ansi "^3.0.0"
+
+webpack-merge@^4.1.0:
+  version "4.1.2"
+  resolved "https://registry.yarnpkg.com/webpack-merge/-/webpack-merge-4.1.2.tgz#5d372dddd3e1e5f8874f5bf5a8e929db09feb216"
+  dependencies:
+    lodash "^4.17.5"
+
+webpack-sources@^0.1.0:
+  version "0.1.5"
+  resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-0.1.5.tgz#aa1f3abf0f0d74db7111c40e500b84f966640750"
+  dependencies:
+    source-list-map "~0.1.7"
+    source-map "~0.5.3"
+
+webpack-sources@^1.0.1:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-1.1.0.tgz#a101ebae59d6507354d71d8013950a3a8b7a5a54"
+  dependencies:
+    source-list-map "^2.0.0"
+    source-map "~0.6.1"
+
+webpack@3:
+  version "3.11.0"
+  resolved "https://registry.yarnpkg.com/webpack/-/webpack-3.11.0.tgz#77da451b1d7b4b117adaf41a1a93b5742f24d894"
+  dependencies:
+    acorn "^5.0.0"
+    acorn-dynamic-import "^2.0.0"
+    ajv "^6.1.0"
+    ajv-keywords "^3.1.0"
+    async "^2.1.2"
+    enhanced-resolve "^3.4.0"
+    escope "^3.6.0"
+    interpret "^1.0.0"
+    json-loader "^0.5.4"
+    json5 "^0.5.1"
+    loader-runner "^2.3.0"
+    loader-utils "^1.1.0"
+    memory-fs "~0.4.1"
+    mkdirp "~0.5.0"
+    node-libs-browser "^2.0.0"
+    source-map "^0.5.3"
+    supports-color "^4.2.1"
+    tapable "^0.2.7"
+    uglifyjs-webpack-plugin "^0.4.6"
+    watchpack "^1.4.0"
+    webpack-sources "^1.0.1"
+    yargs "^8.0.2"
+
+whet.extend@~0.9.9:
+  version "0.9.9"
+  resolved "https://registry.yarnpkg.com/whet.extend/-/whet.extend-0.9.9.tgz#f877d5bf648c97e5aa542fadc16d6a259b9c11a1"
+
+which-module@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/which-module/-/which-module-1.0.0.tgz#bba63ca861948994ff307736089e3b96026c2a4f"
+
+which-module@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a"
+
+which@1, which@^1.1.1, which@^1.2.10, which@^1.2.9:
+  version "1.3.0"
+  resolved "https://registry.yarnpkg.com/which/-/which-1.3.0.tgz#ff04bdfc010ee547d780bec38e1ac1c2777d253a"
+  dependencies:
+    isexe "^2.0.0"
+
+wide-align@^1.1.0:
+  version "1.1.2"
+  resolved "https://registry.yarnpkg.com/wide-align/-/wide-align-1.1.2.tgz#571e0f1b0604636ebc0dfc21b0339bbe31341710"
+  dependencies:
+    string-width "^1.0.2"
+
+window-size@0.1.0:
+  version "0.1.0"
+  resolved "https://registry.yarnpkg.com/window-size/-/window-size-0.1.0.tgz#5438cd2ea93b202efa3a19fe8887aee7c94f9c9d"
+
+wordwrap@0.0.2:
+  version "0.0.2"
+  resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-0.0.2.tgz#b79669bb42ecb409f83d583cad52ca17eaa1643f"
+
+wordwrap@^1.0.0, wordwrap@~1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb"
+
+wordwrap@~0.0.2:
+  version "0.0.3"
+  resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-0.0.3.tgz#a3d5da6cd5c0bc0008d37234bbaf1bed63059107"
+
+wrap-ansi@^2.0.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-2.1.0.tgz#d8fc3d284dd05794fe84973caecdd1cf824fdd85"
+  dependencies:
+    string-width "^1.0.1"
+    strip-ansi "^3.0.1"
+
+wrappy@1:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
+
+write@^0.2.1:
+  version "0.2.1"
+  resolved "https://registry.yarnpkg.com/write/-/write-0.2.1.tgz#5fc03828e264cea3fe91455476f7a3c566cb0757"
+  dependencies:
+    mkdirp "^0.5.1"
+
+ws@1.1.2:
+  version "1.1.2"
+  resolved "https://registry.yarnpkg.com/ws/-/ws-1.1.2.tgz#8a244fa052401e08c9886cf44a85189e1fd4067f"
+  dependencies:
+    options ">=0.0.5"
+    ultron "1.0.x"
+
+ws@^4.0.0:
+  version "4.1.0"
+  resolved "https://registry.yarnpkg.com/ws/-/ws-4.1.0.tgz#a979b5d7d4da68bf54efe0408967c324869a7289"
+  dependencies:
+    async-limiter "~1.0.0"
+    safe-buffer "~5.1.0"
+
+wtf-8@1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/wtf-8/-/wtf-8-1.0.0.tgz#392d8ba2d0f1c34d1ee2d630f15d0efb68e1048a"
+
+xmlhttprequest-ssl@1.5.3:
+  version "1.5.3"
+  resolved "https://registry.yarnpkg.com/xmlhttprequest-ssl/-/xmlhttprequest-ssl-1.5.3.tgz#185a888c04eca46c3e4070d99f7b49de3528992d"
+
+xregexp@2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/xregexp/-/xregexp-2.0.0.tgz#52a63e56ca0b84a7f3a5f3d61872f126ad7a5943"
+
+xtend@^4.0.0, xtend@~4.0.1:
+  version "4.0.1"
+  resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.1.tgz#a5c6d532be656e23db820efb943a1f04998d63af"
+
+y18n@^3.2.1:
+  version "3.2.1"
+  resolved "https://registry.yarnpkg.com/y18n/-/y18n-3.2.1.tgz#6d15fba884c08679c0d77e88e7759e811e07fa41"
+
+y18n@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.0.tgz#95ef94f85ecc81d007c264e190a120f0a3c8566b"
+
+yallist@^2.1.2:
+  version "2.1.2"
+  resolved "https://registry.yarnpkg.com/yallist/-/yallist-2.1.2.tgz#1c11f9218f076089a47dd512f93c6699a6a81d52"
+
+yargs-parser@^5.0.0:
+  version "5.0.0"
+  resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-5.0.0.tgz#275ecf0d7ffe05c77e64e7c86e4cd94bf0e1228a"
+  dependencies:
+    camelcase "^3.0.0"
+
+yargs-parser@^7.0.0:
+  version "7.0.0"
+  resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-7.0.0.tgz#8d0ac42f16ea55debd332caf4c4038b3e3f5dfd9"
+  dependencies:
+    camelcase "^4.1.0"
+
+yargs-parser@^8.1.0:
+  version "8.1.0"
+  resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-8.1.0.tgz#f1376a33b6629a5d063782944da732631e966950"
+  dependencies:
+    camelcase "^4.1.0"
+
+yargs@^10.0.3:
+  version "10.1.2"
+  resolved "https://registry.yarnpkg.com/yargs/-/yargs-10.1.2.tgz#454d074c2b16a51a43e2fb7807e4f9de69ccb5c5"
+  dependencies:
+    cliui "^4.0.0"
+    decamelize "^1.1.1"
+    find-up "^2.1.0"
+    get-caller-file "^1.0.1"
+    os-locale "^2.0.0"
+    require-directory "^2.1.1"
+    require-main-filename "^1.0.1"
+    set-blocking "^2.0.0"
+    string-width "^2.0.0"
+    which-module "^2.0.0"
+    y18n "^3.2.1"
+    yargs-parser "^8.1.0"
+
+yargs@^7.0.0:
+  version "7.1.0"
+  resolved "https://registry.yarnpkg.com/yargs/-/yargs-7.1.0.tgz#6ba318eb16961727f5d284f8ea003e8d6154d0c8"
+  dependencies:
+    camelcase "^3.0.0"
+    cliui "^3.2.0"
+    decamelize "^1.1.1"
+    get-caller-file "^1.0.1"
+    os-locale "^1.4.0"
+    read-pkg-up "^1.0.1"
+    require-directory "^2.1.1"
+    require-main-filename "^1.0.1"
+    set-blocking "^2.0.0"
+    string-width "^1.0.2"
+    which-module "^1.0.0"
+    y18n "^3.2.1"
+    yargs-parser "^5.0.0"
+
+yargs@^8.0.2:
+  version "8.0.2"
+  resolved "https://registry.yarnpkg.com/yargs/-/yargs-8.0.2.tgz#6299a9055b1cefc969ff7e79c1d918dceb22c360"
+  dependencies:
+    camelcase "^4.1.0"
+    cliui "^3.2.0"
+    decamelize "^1.1.1"
+    get-caller-file "^1.0.1"
+    os-locale "^2.0.0"
+    read-pkg-up "^2.0.0"
+    require-directory "^2.1.1"
+    require-main-filename "^1.0.1"
+    set-blocking "^2.0.0"
+    string-width "^2.0.0"
+    which-module "^2.0.0"
+    y18n "^3.2.1"
+    yargs-parser "^7.0.0"
+
+yargs@~3.10.0:
+  version "3.10.0"
+  resolved "https://registry.yarnpkg.com/yargs/-/yargs-3.10.0.tgz#f7ee7bd857dd7c1d2d38c0e74efbd681d1431fd1"
+  dependencies:
+    camelcase "^1.0.2"
+    cliui "^2.1.0"
+    decamelize "^1.0.0"
+    window-size "0.1.0"
+
+yauzl@2.4.1:
+  version "2.4.1"
+  resolved "https://registry.yarnpkg.com/yauzl/-/yauzl-2.4.1.tgz#9528f442dab1b2284e58b4379bb194e22e0c4005"
+  dependencies:
+    fd-slicer "~1.0.1"
+
+yeast@0.1.2:
+  version "0.1.2"
+  resolved "https://registry.yarnpkg.com/yeast/-/yeast-0.1.2.tgz#008e06d8094320c372dbc2f8ed76a0ca6c8ac419"
diff --git a/api/funkwhale_api/federation/management/__init__.py b/po/.gitkeep
similarity index 100%
rename from api/funkwhale_api/federation/management/__init__.py
rename to po/.gitkeep
diff --git a/po/funkwhale.pot b/po/funkwhale.pot
new file mode 100644
index 0000000000000000000000000000000000000000..9a9dd948f7e18af7ba52984ace95f696d75e17d0
--- /dev/null
+++ b/po/funkwhale.pot
@@ -0,0 +1,613 @@
+msgid ""
+msgstr ""
+"Project-Id-Version: Funkwhale 0.8\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2018-03-23 15:49-0700\n"
+"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: LANGUAGE <LL@li.org>\n"
+"Language: \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=INTEGER; plural=EXPRESSION;\n"
+
+msgid "An unknown error occured, ensure your are connected to the internet and your funkwhale instance is up and running"
+msgstr "An unknown error occured, ensure your are connected to the internet and your funkwhale instance is up and running"
+
+msgid "Links"
+msgstr "Links"
+
+msgid "About this instance"
+msgstr "About this instance"
+
+msgid "Official website"
+msgstr "Official website"
+
+msgid "Source code"
+msgstr "Source code"
+
+msgid "Issue tracker"
+msgstr "Issue tracker"
+
+msgid "About funkwhale"
+msgstr "About funkwhale"
+
+msgid "Funkwhale is a free and open-source project run by volunteers. You can help us improve the platform by reporting bugs, suggesting features and share the project with your friends!"
+msgstr "Funkwhale is a free and open-source project run by volunteers. You can help us improve the platform by reporting bugs, suggesting features and share the project with your friends!"
+
+msgid "The funkwhale logo was kindly designed and provided by Francis Gading."
+msgstr "The funkwhale logo was kindly designed and provided by Francis Gading."
+
+msgid "{%0%} favorited a track {%1%}"
+msgstr "{%0%} favorited a track {%1%}"
+
+msgid "from album {%0%}, by {%1%}"
+msgstr "from album {%0%}, by {%1%}"
+
+msgid ", by {%0%}"
+msgstr ", by {%0%}"
+
+msgid "{%0%} listened to a track {%1%}"
+msgstr "{%0%} listened to a track {%1%}"
+
+msgid "Show {%0%} more tracks"
+msgstr "Show {%0%} more tracks"
+
+msgid "Collapse"
+msgstr "Collapse"
+
+msgid "Play all"
+msgstr "Play all"
+
+msgid "{%0%} tracks"
+msgstr "{%0%} tracks"
+
+msgid "Show {%0%} more albums"
+msgstr "Show {%0%} more albums"
+
+msgid "Title"
+msgstr "Title"
+
+msgid "Artist"
+msgstr "Artist"
+
+msgid "Album"
+msgstr "Album"
+
+msgid "Download..."
+msgstr "Download..."
+
+msgid "Download tracks"
+msgstr "Download tracks"
+
+msgid "There is currently no way to download directly multiple tracks from funkwhale as a ZIP archive. However, you can use a command line tools such as {%0%} to easily download a list of tracks."
+msgstr "There is currently no way to download directly multiple tracks from funkwhale as a ZIP archive. However, you can use a command line tools such as {%0%} to easily download a list of tracks."
+
+msgid "Simply copy paste the snippet below into a terminal to launch the download."
+msgstr "Simply copy paste the snippet below into a terminal to launch the download."
+
+msgid "Keep your PRIVATE_TOKEN secret as it gives access to your account."
+msgstr "Keep your PRIVATE_TOKEN secret as it gives access to your account."
+
+msgid "Cancel"
+msgstr "Cancel"
+
+msgid "Add to current queue"
+msgstr "Add to current queue"
+
+msgid "Play"
+msgstr "Play"
+
+msgid "Add to queue"
+msgstr "Add to queue"
+
+msgid "Play next"
+msgstr "Play next"
+
+msgid "Play now"
+msgstr "Play now"
+
+msgid "Previous track"
+msgstr "Previous track"
+
+msgid "Play track"
+msgstr "Play track"
+
+msgid "Pause track"
+msgstr "Pause track"
+
+msgid "Next track"
+msgstr "Next track"
+
+msgid "Unmute"
+msgstr "Unmute"
+
+msgid "Mute"
+msgstr "Mute"
+
+msgid "Looping disabled. Click to switch to single-track looping."
+msgstr "Looping disabled. Click to switch to single-track looping."
+
+msgid "Looping on a single track. Click to switch to whole queue looping."
+msgstr "Looping on a single track. Click to switch to whole queue looping."
+
+msgid "Looping on whole queue. Click to disable looping."
+msgstr "Looping on whole queue. Click to disable looping."
+
+msgid "Shuffle your queue"
+msgstr "Shuffle your queue"
+
+msgid "Clear your queue"
+msgstr "Clear your queue"
+
+msgid "Search for some music"
+msgstr "Search for some music"
+
+msgid "Artists"
+msgstr "Artists"
+
+msgid "Sorry, we did not found any artist matching your query"
+msgstr "Sorry, we did not found any artist matching your query"
+
+msgid "Albums"
+msgstr "Albums"
+
+msgid "Sorry, we did not found any album matching your query"
+msgstr "Sorry, we did not found any album matching your query"
+
+msgid "Log in to your Funkwhale account"
+msgstr "Log in to your Funkwhale account"
+
+msgid "We cannot log you in"
+msgstr "We cannot log you in"
+
+msgid "Please double-check your username/password couple is correct"
+msgstr "Please double-check your username/password couple is correct"
+
+msgid "An unknown error happend, this can mean the server is down or cannot be reached"
+msgstr "An unknown error happend, this can mean the server is down or cannot be reached"
+
+msgid "Username or email"
+msgstr "Username or email"
+
+msgid "Password"
+msgstr "Password"
+
+msgid "Login"
+msgstr "Login"
+
+msgid "Create an account"
+msgstr "Create an account"
+
+msgid "Are you sure you want to log out?"
+msgstr "Are you sure you want to log out?"
+
+msgid "You are currently logged in as {%0%}"
+msgstr "You are currently logged in as {%0%}"
+
+msgid "Yes, log me out!"
+msgstr "Yes, log me out!"
+
+msgid "Registered since {%0%}"
+msgstr "Registered since {%0%}"
+
+msgid "This is you!"
+msgstr "This is you!"
+
+msgid "Staff member"
+msgstr "Staff member"
+
+msgid "Settings..."
+msgstr "Settings..."
+
+msgid "Account settings"
+msgstr "Account settings"
+
+msgid "Settings updated"
+msgstr "Settings updated"
+
+msgid "We cannot save your settings"
+msgstr "We cannot save your settings"
+
+msgid "Update settings"
+msgstr "Update settings"
+
+msgid "Change my password"
+msgstr "Change my password"
+
+msgid "Cannot change your password"
+msgstr "Cannot change your password"
+
+msgid "Please double-check your password is correct"
+msgstr "Please double-check your password is correct"
+
+msgid "Old password"
+msgstr "Old password"
+
+msgid "New password"
+msgid "New password"
+
+msgid "Change password"
+msgstr "Change password"
+
+msgid "Activity visibility"
+msgstr "Activity visibility"
+
+msgid "Determine the visibility level of your activity"
+msgstr "Determine the visibility level of your activity"
+
+msgid "Nobody except me"
+msgstr "Nobody except me"
+
+msgid "Everyone on this instance"
+msgstr "Everyone on this instance"
+
+msgid "Create a funkwhale account"
+msgstr "Create a funkwhale account"
+
+msgid "We cannot create your account"
+msgstr "We cannot create your account"
+
+msgid "Username"
+msgstr "Username"
+
+msgid "Email"
+msgstr "Email"
+
+msgid "Create my account"
+msgstr "Create my account"
+
+msgid "Registration is currently disabled on this instance, please try again later."
+msgstr "Registration is currently disabled on this instance, please try again later."
+
+msgid "Do you want to confirm this action?"
+msgstr "Do you want to confirm this action?"
+
+msgid "Confirm"
+msgstr "Confirm"
+
+msgid "Expand"
+msgstr "Expand"
+
+msgid "Loading your favorites..."
+msgstr "Loading your favorites..."
+
+msgid "{%0%} favorites"
+msgstr "{%0%} favorites"
+
+msgid "Ordering"
+msgstr "Ordering"
+
+msgid "Ordering direction"
+msgstr "Ordering direction"
+
+msgid "Ascending"
+msgstr "Ascending"
+
+msgid "Descending"
+msgstr "Descending"
+
+msgid "Results per page"
+msgstr "Results per page"
+
+msgid "Track name"
+msgstr "Track name"
+
+msgid "Album name"
+msgstr "Album name"
+
+msgid "Artist name"
+msgstr "Artist name"
+
+msgid "In favorites"
+msgstr "In favorites"
+
+msgid "Add to favorites"
+msgstr "Add to favorites"
+
+msgid "Remove from favorites"
+msgstr "Remove from favorites"
+
+msgid "User activity"
+msgstr "User activity"
+
+msgid "users"
+msgstr "users"
+
+msgid "tracks listened"
+msgstr "tracks listened"
+
+msgid "Tracks favorited"
+msgstr "Tracks favorited"
+
+msgid "hours of music"
+msgstr "hours of music"
+
+msgid "Artists"
+msgstr "Artists"
+
+msgid "Albums"
+msgstr "Albums"
+
+msgid "tracks"
+msgstr "tracks"
+
+msgid "Filter album types"
+msgstr "Filter album types"
+
+msgid "Query template"
+msgstr "Query template"
+
+msgid "Live"
+msgstr "Live"
+
+msgid "Compilation"
+msgstr "Compilation"
+
+msgid "EP"
+msgstr "EP"
+
+msgid "Single"
+msgstr "Single"
+
+msgid "Other"
+msgstr "Other"
+
+msgid "Job ID"
+msgstr "Job ID"
+
+msgid "Recording MusicBrainz ID"
+msgstr "Recording MusicBrainz ID"
+
+msgid "Source"
+msgstr "Source"
+
+msgid "Track"
+msgstr "Track"
+
+msgid "Previous"
+msgstr "Previous"
+
+msgid "Next"
+msgstr "Next"
+
+msgid "ID"
+msgstr "ID"
+
+msgid "Launch date"
+msgstr "Launch date"
+
+msgid "Jobs"
+msgstr "Jobs"
+
+msgid "Status"
+msgstr "Status"
+
+msgid "Select files to upload..."
+msgstr "Select files to upload..."
+
+msgid "Start Upload"
+msgstr "Start Upload"
+
+msgid "Stop Upload"
+msgstr "Stop Upload"
+
+msgid "Once all your files are uploaded, simply head over {%0%} to check the import status."
+msgstr "Once all your files are uploaded, simply head over {%0%} to check the import status."
+
+msgid "import detail page"
+msgstr "import detail page"
+
+msgid "File name"
+msgstr "File name"
+
+msgid "Size"
+msgstr "Size"
+
+msgid "Success"
+msgstr "Success"
+
+msgid "Uploading..."
+msgstr "Uploading..."
+
+msgid "Pending"
+msgstr "Pending"
+
+msgid "Import source"
+msgstr "Import source"
+
+msgid "Uploaded files or external source"
+msgstr "Uploaded files or external source"
+
+msgid "Metadata"
+msgstr "Metadata"
+
+msgid "Grab corresponding metadata"
+msgstr "Grab corresponding metadata"
+
+msgid "Music"
+msgstr "Music"
+
+msgid "Select relevant sources or files for import"
+msgstr "Select relevant sources or files for import"
+
+msgid "Previous step"
+msgstr "Previous step"
+
+msgid "Next step"
+msgstr "Next step"
+
+msgid "Import {%0%} tracks"
+msgstr "Import {%0%} tracks"
+
+msgid "First, choose where you want to import the music from:"
+msgstr "First, choose where you want to import the music from:"
+
+msgid "External source. Supported backends:"
+msgstr "External source. Supported backends:"
+
+msgid "File upload"
+msgstr "File upload"
+
+msgid "Search an entity you want to import:"
+msgstr "Search an entity you want to import:"
+
+msgid "Input a MusicBrainz ID manually:"
+msgstr "Input a MusicBrainz ID manually:"
+
+msgid "Or"
+msgstr "Or"
+
+msgid "You will import:"
+msgstr "You will import:"
+
+msgid "You can also skip this step and enter metadata manually."
+msgstr "You can also skip this step and enter metadata manually."
+
+msgid "Metadata is the data related to the music you want to import. This includes all the information about the artists, albums and tracks. In order to have a high quality library, it is recommended to grab data from the {%0%} project, which you can think about as the Wikipedia of music."
+msgstr "Metadata is the data related to the music you want to import. This includes all the information about the artists, albums and tracks. In order to have a high quality library, it is recommended to grab data from the {%0%} project, which you can think about as the Wikipedia of music."
+
+msgid "Music request"
+msgstr "Music request"
+
+msgid "This import will be associated with the music request below. After the import is finished, the request will be marked as fulfilled."
+msgstr "This import will be associated with the music request below. After the import is finished, the request will be marked as fulfilled."
+
+msgid "Album {%0%} ({%1%} tracks) by {%2%}"
+msgstr "Album {%0%} ({%1%} tracks) by {%2%}"
+
+msgid "Import this release"
+msgstr "Import this release"
+
+msgid "Import this track"
+msgstr "Import this track"
+
+msgid "Source"
+msgstr "Source"
+
+msgid "Result {%0%}/{%1%}"
+msgstr "Result {%0%}/{%1%}"
+
+msgid "Search query"
+msgstr "Search query"
+
+msgid "Imported URL"
+msgstr "Imported URL"
+
+msgid "Album containing {%0%} tracks, by {%1%}"
+msgstr "Album containing {%0%} tracks, by {%1%}"
+
+msgid "Search on Wikipedia"
+msgstr "Search on Wikipedia"
+
+msgid "View on MusicBrainz"
+msgstr "View on MusicBrainz"
+
+msgid "{%0%} tracks in {%1%} albums"
+msgstr "{%0%} tracks in {%1%} albums"
+
+msgid "Play all albums"
+msgstr "Play all albums"
+
+msgid "Albums by this artist"
+msgstr "Albums by this artist"
+
+msgid "Browsing artists"
+msgstr "Browsing artists"
+
+msgid "Search"
+msgstr "Search"
+
+msgid "Latest artists"
+msgstr "Latest artists"
+
+msgid "Radios"
+msgstr "Radios"
+
+msgid "Music requests"
+msgstr "Music requests"
+
+msgid "Browse"
+msgstr "Browse"
+
+msgid "Playlists"
+msgstr "Playlists"
+
+msgid "Requests"
+msgstr "Requests"
+
+msgid "Import"
+msgstr "Import"
+
+msgid "Import batches"
+msgstr "Import batches"
+
+msgid "Browsing radios"
+msgstr "Browsing radios"
+
+msgid "Create your own radio"
+msgstr "Create your own radio"
+
+msgid "From album {%0%} by {%1%}"
+msgstr "From album {%0%} by {%1%}"
+
+msgid "Download"
+msgstr "Download"
+
+msgid "Lyrics"
+msgstr "Lyrics"
+
+msgid "No lyrics available for this track."
+msgstr "No lyrics available for this track."
+
+msgid "Search on lyrics.wikia.com"
+msgstr "Search on lyrics.wikia.com"
+
+msgid "Builder"
+msgstr "Builder"
+
+msgid "You can use this interface to build your own custom radio, which will play tracks according to your criteria"
+msgstr "You can use this interface to build your own custom radio, which will play tracks according to your criteria"
+
+msgid "Radio name"
+msgstr "Radio name"
+
+msgid "Display publicly"
+msgstr "Display publicly"
+
+msgid "Save"
+msgstr "Save"
+
+msgid "Add filters to customize your radio"
+msgstr "Add filters to customize your radio"
+
+msgid "Select a filter"
+msgstr "Select a filter"
+
+msgid "Add filter"
+msgstr "Add filter"
+
+msgid "Filter name"
+msgstr "Filter name"
+
+msgid "Exclude"
+msgstr "Exclude"
+
+msgid "Config"
+msgstr "Config"
+
+msgid "Candidates"
+msgstr "Candidates"
+
+msgid "Actions"
+msgstr "Actions"
+
+msgid "{%0%} tracks matching combined filters"
+msgstr "{%0%} tracks matching combined filters"
+
+msgid "Track matching filter"
+msgstr "Track matching filter"
+
+msgid "Remove"
+msgstr "Remove"