Add cli in the repo to truly become a MONOREPO 🎉

This commit is contained in:
Simon Prévost 2019-03-04 17:16:50 -05:00
parent b2750d0b60
commit be9f6bed83
62 changed files with 5933 additions and 0 deletions

1
cli/.eslintignore Normal file
View File

@ -0,0 +1 @@
/lib

3
cli/.eslintrc Normal file
View File

@ -0,0 +1,3 @@
{
"extends": "oclif"
}

6
cli/.gitignore vendored Normal file
View File

@ -0,0 +1,6 @@
*-debug.log
*-error.log
.oclif.manifest.json
/lib
/node_modules
/tmp

9
cli/LICENSE.md Normal file
View File

@ -0,0 +1,9 @@
Copyright (c) 2018, Mirego All rights reserved.
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
Neither the name of the Mirego nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

173
cli/README.md Normal file
View File

@ -0,0 +1,173 @@
Accent CLI
======
[![Version](https://img.shields.io/npm/v/accent-cli.svg)](https://npmjs.org/package/accent-cli)
[![Build Status](https://img.shields.io/travis/v/accent-cli.svg?branch=master)](https://travis-ci.com/mirego/accent-cli)
<!-- toc -->
* [Usage](#usage)
* [Configuration](#configuration)
* [Commands](#commands)
* [License](#license)
* [About Mirego](#about-mirego)
<!-- tocstop -->
# Usage
<!-- usage -->
```sh-session
$ npm install -g accent-cli
$ accent COMMAND
running command...
$ accent (-v|--version|version)
accent-cli/0.6.0 darwin-x64 node-v9.5.0
$ accent --help [COMMAND]
USAGE
$ accent COMMAND
...
```
<!-- usagestop -->
# Configuration
accent-cli reads from a `accent.json` file. The file should contain valid JSON representing the configuration of your project.
## Example
```
{
"apiUrl": "http://your.accent.instance",
"apiKey": "2nziVSaa8yUJxLkwoZA",
"files": [
{
"language": "fr",
"format": "json",
"source": "localization/fr/*.json",
"target": "localization/%slug%/%original_file_name%.json",
"hooks": {
"afterSync": "touch sync-done.txt"
}
}
]
}
```
## Document configuration
Each operation section `sync` and `addTranslations` can contain the following object:
- `language`: The identifier of the documents language
- `format`: The format of the document
- `source`: The path of the document. This can contain glob pattern (See [the node glob library] used as a dependancy (https://github.com/isaacs/node-glob))
- `target`: Path of the target languages
- `hooks`: List of hooks to be run
## Hooks
Here is a list of available hooks. Those are self-explanatory
- `beforeSync`
- `afterSync`
- `beforeExport`
- `afterExport`
# Commands
<!-- commands -->
* [`accent export`](#accent-export)
* [`accent help [COMMAND]`](#accent-help-command)
* [`accent jipt PSEUDOLANGUAGENAME`](#accent-jipt-pseudolanguagename)
* [`accent stats`](#accent-stats)
* [`accent sync`](#accent-sync)
## `accent export`
Export files from Accent and write them to your local filesystem
```
USAGE
$ accent export
OPTIONS
--order-by=index|key-asc [default: index] Will be used in the export call as the order of the keys
EXAMPLE
$ accent export
```
## `accent help [COMMAND]`
display help for accent
```
USAGE
$ accent help [COMMAND]
ARGUMENTS
COMMAND command to show help for
OPTIONS
--all see all commands in CLI
```
_See code: [@oclif/plugin-help](https://github.com/oclif/plugin-help/blob/v2.1.4/src/commands/help.ts)_
## `accent jipt PSEUDOLANGUAGENAME`
Export jipt files from Accent and write them to your local filesystem
```
USAGE
$ accent jipt PSEUDOLANGUAGENAME
ARGUMENTS
PSEUDOLANGUAGENAME The pseudo language for in-place-translation-editing
EXAMPLE
$ accent jipt
```
## `accent stats`
Fetch stats from the API and display it beautifully
```
USAGE
$ accent stats
EXAMPLE
$ accent stats
```
## `accent sync`
Sync files in Accent and write them to your local filesystem
```
USAGE
$ accent sync
OPTIONS
--add-translations Add translations in Accent to help translators if you already have translated
strings
--merge-type=smart|passive|force [default: smart] Will be used in the add translations call as the "merge_type" param
--order-by=index|key-asc [default: index] Will be used in the export call as the order of the keys
--sync-type=smart|passive [default: smart] Will be used in the sync call as the "sync_type" param
--write Write the file from the export _after_ the operation
EXAMPLE
$ accent sync
```
<!-- commandsstop -->
# License
`accent-cli` is © 2019 [Mirego](http://www.mirego.com) and may be freely distributed under the [New BSD license](http://opensource.org/licenses/BSD-3-Clause). See the [`LICENSE.md`](https://github.com/mirego/accent-cli/blob/master/LICENSE.md) file.
# About Mirego
[Mirego](http://mirego.com) is a team of passionate people who believe that work is a place where you can innovate and have fun. Were a team of [talented people](http://life.mirego.com) who imagine and build beautiful Web and mobile applications. We come together to share ideas and [change the world](http://mirego.org).
We also [love open-source software](http://open.mirego.com) and we try to give back to the community as much as we can.

4
cli/bin/run Executable file
View File

@ -0,0 +1,4 @@
#!/usr/bin/env node
require('@oclif/command').run()
.catch(require('@oclif/errors/handle'))

3
cli/bin/run.cmd Normal file
View File

@ -0,0 +1,3 @@
@echo off
node "%~dp0\run" %*

View File

@ -0,0 +1,24 @@
{
"files": [
{
"language": "fr",
"format": "json",
"source": "core/*.json",
"target": "core/%original_file_name%-%slug%.json",
"hooks": {
"beforeSync": [
"rm -rf core",
"mkdir -p core",
"ruby core.rb unmerge translations.json fr > core/core-is-awesome.json"
],
"beforeAddTranslations": [
"ruby core.rb unmerge translations.json en > core/core-is-awesome-en.json"
],
"afterExport": [
"ruby core.rb merge core/core-is-awesome-en.json core/core-is-awesome-fr.json > translations.json",
"rm -rf core"
]
}
}
]
}

49
cli/examples/core/core.rb Normal file
View File

@ -0,0 +1,49 @@
# Merge and unmerge files to be "core translations" compliant and Accent compliant.
# ## Merge
# The file used in this command contains en and fr translations and outputs
# "key" => value in the specified language.
#
# Given the file en.json:
# {"key": "foo"}
# Given the file fr.json:
# {"key": "bar"}
# ruby core.rb merge en.json fr.json
# {
# "key": {
# "en": "bar",
# "fr": "foo"
# }
# }
#
# ## Unmerge
# The file used in this command contains en and fr translations and outputs
# "key" => value in the specified language.
#
# Given the file:
# {
# "key": {
# "en": "bar",
# "fr": "foo"
# }
# }
# `ruby core.rb unmerge file.json fr`
#
# {
# "key": "foo"
# }
require 'json'
if ARGV[0] === 'merge'
en_json = JSON.parse(File.read(ARGV[1]))
fr_json = JSON.parse(File.read(ARGV[2]))
output = en_json.each_with_object({}) { |(key, value), memo| memo[key] = {fr: fr_json[key], en: value} }
puts JSON.pretty_generate(output)
end
if ARGV[0] === 'unmerge'
output = JSON.parse(File.read(ARGV[1])).each_with_object({}) { |(key, value), memo| memo[key] = value[ARGV[2]] }
puts JSON.pretty_generate(output)
end

View File

@ -0,0 +1,6 @@
{
"key": {
"fr": "valeur",
"en": "value"
}
}

View File

@ -0,0 +1,10 @@
{
"files": [
{
"language": "en",
"format": "json",
"source": "translations/en.json",
"target": "translations/%slug%.json"
}
]
}

View File

@ -0,0 +1,5 @@
{
"key": {
"nested": "Value"
}
}

View File

@ -0,0 +1,5 @@
{
"key": {
"nested": "Valeur"
}
}

View File

@ -0,0 +1,10 @@
{
"files": [
{
"language": "en",
"format": "gettext",
"source": "priv/en/*.po",
"target": "priv/%slug%/%original_file_name%.po"
}
]
}

View File

@ -0,0 +1,6 @@
msgid ""
msgstr ""
"Language: en"
msgid "key"
msgstr "Value"

View File

@ -0,0 +1,6 @@
msgid ""
msgstr ""
"Language: fr"
msgid "key"
msgstr "Valeur"

View File

@ -0,0 +1,2 @@
output-directory: config/locales
input-directory: config/locales

View File

@ -0,0 +1,3 @@
source 'https://rubygems.org'
gem 'aigu'

View File

@ -0,0 +1,17 @@
GEM
remote: https://rubygems.org/
specs:
aigu (1.2)
nokogiri (~> 1.6)
mini_portile2 (2.3.0)
nokogiri (1.8.5)
mini_portile2 (~> 2.3.0)
PLATFORMS
ruby
DEPENDENCIES
aigu
BUNDLED WITH
1.16.2

View File

@ -0,0 +1,29 @@
{
"files": [
{
"language": "en",
"format": "json",
"source": "aigu/*.json",
"target": "aigu/%original_file_name%-%slug%.json",
"hooks": {
"beforeSync": [
"rm -rf aigu",
"mkdir -p aigu",
"aigu rails-export --locale=en --output-file=aigu/aigu-is-awesome.json >/dev/null 2>&1"
],
"beforeAddTranslations": [
"aigu rails-export --locale=fr --output-file=aigu/aigu-is-awesome-fr.json >/dev/null 2>&1"
],
"beforeExport": [
"rm -rf aigu",
"mkdir -p aigu"
],
"afterExport": [
"aigu rails-import --locale=en --input-file=aigu/aigu-is-awesome-en.json >/dev/null 2>&1",
"aigu rails-import --locale=fr --input-file=aigu/aigu-is-awesome-fr.json >/dev/null 2>&1",
"rm -rf aigu"
]
}
}
]
}

View File

@ -0,0 +1,4 @@
---
en:
my_file:
key: Value

View File

@ -0,0 +1,4 @@
---
fr:
my_file:
key: Valeur

View File

@ -0,0 +1,10 @@
{
"files": [
{
"language": "en",
"format": "rails_yml",
"source": "config/locales/en/*.yml",
"target": "config/locales/%slug%/%original_file_name%.yml"
}
]
}

View File

@ -0,0 +1,4 @@
---
en:
my_file:
key: Value

View File

@ -0,0 +1,4 @@
---
fr:
my_file:
key: Valeur

View File

@ -0,0 +1,10 @@
{
"files": [
{
"language": "fr",
"format": "json",
"source": "src/locales/fr/*.json",
"target": "src/locales/%slug%/%original_file_name%.json"
}
]
}

View File

@ -0,0 +1,6 @@
{
"nav": {
"organizations": "{^nav.organizations@admin}",
"reservations": "{^nav.reservations@admin}"
}
}

View File

@ -0,0 +1,239 @@
{
"navigation": {
"switchLanguage": {
"en": "{acc:navigation.switchLanguage.en@common}",
"fr": "{acc:navigation.switchLanguage.fr@common}"
},
"search": "{acc:navigation.search@common}",
"admin": {
"organizations": "{acc:navigation.admin.organizations@common}",
"reservations": "{acc:navigation.admin.reservations@common}",
"title": "{acc:navigation.admin.title@common}"
},
"owner": {
"equipments": "{acc:navigation.owner.equipments@common}",
"parkingLots": "{acc:navigation.owner.parkingLots@common}",
"reservations": "{acc:navigation.owner.reservations@common}",
"title": "{acc:navigation.owner.title@common}"
},
"renter": {
"reservations": "{acc:navigation.renter.reservations@common}",
"title": "{acc:navigation.renter.title@common}"
},
"account": {
"administration": "{acc:navigation.account.administration@common}",
"organization": "{acc:navigation.account.organization@common}"
},
"logout": "{acc:navigation.logout@common}",
"login": "{acc:navigation.login@common}"
},
"languages": {
"english": {
"title": "{acc:languages.english.title@common}",
"code": "{acc:languages.english.code@common}"
},
"french": {
"title": "{acc:languages.french.title@common}",
"code": "{acc:languages.french.code@common}"
}
},
"mileage": {
"KILOMETERS": "{acc:mileage.KILOMETERS@common}",
"MILES": "{acc:mileage.MILES@common}"
},
"notFound": {
"goHome": "{acc:notFound.goHome@common}",
"message": "{acc:notFound.message@common}",
"title": "{acc:notFound.title@common}"
},
"reservations": {
"activities": {
"chronology": {
"application": "{acc:reservations.activities.chronology.application@common}",
"date": "{acc:reservations.activities.chronology.date@common}",
"synchronization": "{acc:reservations.activities.chronology.synchronization@common}",
"title": "{acc:reservations.activities.chronology.title@common}"
},
"damages": {
"date": "{acc:reservations.activities.damages.date@common}",
"noDamages": "{acc:reservations.activities.damages.noDamages@common}",
"title": "{acc:reservations.activities.damages.title@common}"
},
"inspection": {
"noConditionReports": "{acc:reservations.activities.inspection.noConditionReports@common}",
"title": "{acc:reservations.activities.inspection.title@common}"
},
"licencePlate": {
"title": "{acc:reservations.activities.licencePlate.title@common}"
},
"location": {
"addressTitle": "{acc:reservations.activities.location.addressTitle@common}",
"error": "{acc:reservations.activities.location.error@common}",
"notFound": "{acc:reservations.activities.location.notFound@common}",
"notProvided": "{acc:reservations.activities.location.notProvided@common}",
"title": "{acc:reservations.activities.location.title@common}"
},
"odometer": {
"notProdived": "{acc:reservations.activities.odometer.notProdived@common}",
"title": "{acc:reservations.activities.odometer.title@common}"
},
"review": {
"reasons": {
"alreadyPacked": "{acc:reservations.activities.review.reasons.alreadyPacked@common}",
"dirty": "{acc:reservations.activities.review.reasons.dirty@common}",
"damaged": "{acc:reservations.activities.review.reasons.damaged@common}",
"snow": "{acc:reservations.activities.review.reasons.snow@common}",
"copled": "{acc:reservations.activities.review.reasons.copled@common}",
"notCompliant": "{acc:reservations.activities.review.reasons.notCompliant@common}",
"other": "{acc:reservations.activities.review.reasons.other@common}"
},
"title": "{acc:reservations.activities.review.title@common}"
}
},
"assignees": {
"invitationList": {
"empty": "{acc:reservations.assignees.invitationList.empty@common}"
},
"sendInvite": {
"form": {
"actions": {
"submit": {
"caption": "{acc:reservations.assignees.sendInvite.form.actions.submit.caption@common}"
}
},
"errors": {
"failed": "{acc:reservations.assignees.sendInvite.form.errors.failed@common}"
},
"fields": {
"driverName": {
"errors": {
"required": "{acc:reservations.assignees.sendInvite.form.fields.driverName.errors.required@common}"
},
"label": "{acc:reservations.assignees.sendInvite.form.fields.driverName.label@common}",
"placeholder": "{acc:reservations.assignees.sendInvite.form.fields.driverName.placeholder@common}"
},
"phoneNumber": {
"errors": {
"invalid": "{acc:reservations.assignees.sendInvite.form.fields.phoneNumber.errors.invalid@common}",
"required": "{acc:reservations.assignees.sendInvite.form.fields.phoneNumber.errors.required@common}"
},
"label": "{acc:reservations.assignees.sendInvite.form.fields.phoneNumber.label@common}",
"placeholder": "{acc:reservations.assignees.sendInvite.form.fields.phoneNumber.placeholder@common}"
}
}
}
}
},
"stateFilter": {
"all": "{acc:reservations.stateFilter.all@common}",
"current": "{acc:reservations.stateFilter.current@common}",
"cancelled": "{acc:reservations.stateFilter.cancelled@common}",
"done": "{acc:reservations.stateFilter.done@common}",
"upcoming": "{acc:reservations.stateFilter.upcoming@common}",
"late_activation": "{acc:reservations.stateFilter.late_activation@common}",
"late_deactivation": "{acc:reservations.stateFilter.late_deactivation@common}"
},
"statusBadge": {
"activated": "{acc:reservations.statusBadge.activated@common}",
"deactivated": "{acc:reservations.statusBadge.deactivated@common}",
"lateActivation": "{acc:reservations.statusBadge.lateActivation@common}",
"lateDeactivation": "{acc:reservations.statusBadge.lateDeactivation@common}",
"upcoming": "{acc:reservations.statusBadge.upcoming@common}"
}
},
"search": "{acc:search@common}",
"searchMap": {
"departure": "{acc:searchMap.departure@common}"
},
"timeAgo": "{acc:timeAgo@common}",
"errors": {
"overlap_availabilities": "{acc:errors.overlap_availabilities@common}",
"overlap_reservations": "{acc:errors.overlap_reservations@common}",
"equipment_not_available": "{acc:errors.equipment_not_available@common}"
},
"form": {
"actions": {
"delete": {
"caption": "{acc:form.actions.delete.caption@common}"
},
"cancel": {
"caption": "{acc:form.actions.cancel.caption@common}"
}
}
},
"currency": {
"prefixSymbol": "{acc:currency.prefixSymbol@common}",
"suffixSymbol": "{acc:currency.suffixSymbol@common}"
},
"trailerTypes": {
"dry_van": "{acc:trailerTypes.dry_van@common}",
"plate_dry_van": "{acc:trailerTypes.plate_dry_van@common}",
"storage_van": "{acc:trailerTypes.storage_van@common}",
"reefer_van": "{acc:trailerTypes.reefer_van@common}",
"heated_van": "{acc:trailerTypes.heated_van@common}",
"flatbed": "{acc:trailerTypes.flatbed@common}",
"dropdeck": "{acc:trailerTypes.dropdeck@common}"
},
"country": {
"canada": {
"title": "{acc:country.canada.title@common}",
"code": "{acc:country.canada.code@common}",
"province": {
"ontario": {
"title": "{acc:country.canada.province.ontario.title@common}",
"code": "{acc:country.canada.province.ontario.code@common}"
},
"quebec": {
"title": "{acc:country.canada.province.quebec.title@common}",
"code": "{acc:country.canada.province.quebec.code@common}"
},
"britishColumbia": {
"title": "{acc:country.canada.province.britishColumbia.title@common}",
"code": "{acc:country.canada.province.britishColumbia.code@common}"
},
"alberta": {
"title": "{acc:country.canada.province.alberta.title@common}",
"code": "{acc:country.canada.province.alberta.code@common}"
},
"manitoba": {
"title": "{acc:country.canada.province.manitoba.title@common}",
"code": "{acc:country.canada.province.manitoba.code@common}"
},
"saskatchewan": {
"title": "{acc:country.canada.province.saskatchewan.title@common}",
"code": "{acc:country.canada.province.saskatchewan.code@common}"
},
"novaScotia": {
"title": "{acc:country.canada.province.novaScotia.title@common}",
"code": "{acc:country.canada.province.novaScotia.code@common}"
},
"newBrunswick": {
"title": "{acc:country.canada.province.newBrunswick.title@common}",
"code": "{acc:country.canada.province.newBrunswick.code@common}"
},
"newfoundlandAndLabrador": {
"title": "{acc:country.canada.province.newfoundlandAndLabrador.title@common}",
"code": "{acc:country.canada.province.newfoundlandAndLabrador.code@common}"
},
"princeEdwardIsland": {
"title": "{acc:country.canada.province.princeEdwardIsland.title@common}",
"code": "{acc:country.canada.province.princeEdwardIsland.code@common}"
},
"northwestTerritories": {
"title": "{acc:country.canada.province.northwestTerritories.title@common}",
"code": "{acc:country.canada.province.northwestTerritories.code@common}"
},
"nunavut": {
"title": "{acc:country.canada.province.nunavut.title@common}",
"code": "{acc:country.canada.province.nunavut.code@common}"
},
"yukon": {
"title": "{acc:country.canada.province.yukon.title@common}",
"code": "{acc:country.canada.province.yukon.code@common}"
}
}
}
},
"appTitle": "{acc:appTitle@common}",
"fatalError": "{acc:fatalError@common}"
}

View File

@ -0,0 +1,70 @@
{
"edit": {
"actions": {
"actAsOrganization": "Manage"
},
"errors": {
"organizationNotFound": "The organization doesnt exist"
},
"title": "Edit Organization"
},
"form": {
"actions": {
"submit": {
"caption": "Submit"
}
},
"fields": {
"email": {
"errors": {
"invalid": "Must be a valid email address"
},
"label": "Email Address"
},
"isOwner": {
"label": "Owner"
},
"isRenter": {
"label": "Renter"
},
"name": {
"errors": {
"required": "You must enter a name"
},
"label": "Name"
},
"phoneNumber": {
"errors": {
"invalid": "Must be in the form of: +15553219876"
},
"label": "Phone Number"
}
},
"sections": {
"informations": {
"title": "Informations"
}
}
},
"index": {
"actions": {
"new": "New Organization"
},
"alerts": {
"editOrganizationSuccess": "The organization was updated with success",
"newOrganizationSuccess": "The organization was created with success",
"noOrganization": "There are currently no organizations"
},
"organizationList": {
"header": {
"email": "Email Address",
"name": "Name",
"phoneNumber": "Phone Number"
}
},
"title": "Organizations"
},
"new": {
"title": "New Organization"
}
}

View File

@ -0,0 +1,239 @@
{
"appTitle": "vHub",
"fatalError": "An unexpected error has happened. Please try again later.",
"navigation": {
"admin": {
"organizations": "Organizations",
"reservations": "Reservations",
"title": "Admin"
},
"owner": {
"equipments": "Equipments",
"parkingLots": "Parking Lots",
"reservations": "Reservations",
"title": "Owner"
},
"renter": {
"reservations": "Reservations",
"title": "Renter"
},
"account": {
"administration": "Administration",
"organization": "Manage Organization"
},
"login": "Login",
"logout": "Logout",
"switchLanguage": {
"en": "English",
"fr": "Français"
},
"search": "Search"
},
"languages": {
"english": {
"title": "English",
"code": "en"
},
"french": {
"title": "French",
"code": "fr"
}
},
"mileage": {
"KILOMETERS": "km",
"MILES": "mi"
},
"notFound": {
"goHome": "Go back to homepage",
"message": "The page you are trying to reach is not available or does not exist.",
"title": "Page Not Found"
},
"reservations": {
"activities": {
"chronology": {
"application": "Device time",
"date": "{{date, DD-MM-YYYY, H:mm}}",
"synchronization": "Synchronized at",
"title": "Chronology"
},
"damages": {
"date": "{{date, D MMMM YYYY, H:mm}}",
"noDamages": "No damage declared",
"title": "Damages"
},
"inspection": {
"noConditionReports": "No condition reports declared",
"title": "Inspection"
},
"licencePlate": {
"title": "License Plate"
},
"location": {
"addressTitle": "Drivers position",
"error": "Cannot resolve the address. Try again later.",
"notFound": "No addresses were found at this location",
"notProvided": "Not provided",
"title": "Location"
},
"odometer": {
"notProdived": "No information was provided.",
"title": "Odometer"
},
"review": {
"reasons": {
"alreadyPacked": "Not empty",
"dirty": "Dirty interior",
"damaged": "Severely damaged",
"snow": "Covered with snow",
"copled": "Coupled or inaccessible",
"notCompliant": "Not road compliant",
"other": "Other"
},
"title": "Review"
}
},
"assignees": {
"invitationList": {
"empty": "No invitations were sent yet"
},
"sendInvite": {
"form": {
"actions": {
"submit": {
"caption": "Send"
}
},
"errors": {
"failed": "We cannot send the SMS at the moment. Try again later."
},
"fields": {
"driverName": {
"errors": {
"required": "You must enter a name"
},
"label": "Name",
"placeholder": "John Doe"
},
"phoneNumber": {
"errors": {
"invalid": "Must be in the form of: +15553219876",
"required": "You must enter a phone number"
},
"label": "Phone Number",
"placeholder": "+15553219876"
}
}
}
}
},
"stateFilter": {
"all": "All",
"current": "In Progress",
"cancelled": "Cancelled",
"done": "Done",
"upcoming": "Upcoming",
"late_activation": "Late activation",
"late_deactivation": "Late deactivation"
},
"statusBadge": {
"activated": "Activated at {{date, DD/MM/YYYY HH:mm}}",
"deactivated": "Deactivated at {{date, DD/MM/YYYY HH:mm}}",
"lateActivation": "Late activation {{date, DD/MM/YYYY}}",
"lateDeactivation": "Late deactivation {{date, DD/MM/YYYY}}",
"upcoming": "Upcoming"
}
},
"search": "Search",
"searchMap": {
"departure": "Departure"
},
"timeAgo": "{{distance}} ago",
"errors": {
"overlap_availabilities": "Selected dates overlap an existing availability",
"overlap_reservations": "Selected dates overlap an existing reservation",
"equipment_not_available": "The equipment is not available for those dates"
},
"form": {
"actions": {
"delete": {
"caption": "Delete"
},
"cancel": {
"caption": "Cancel"
}
}
},
"currency": {
"prefixSymbol": "$",
"suffixSymbol": ""
},
"trailerTypes": {
"dry_van": "Dry Van",
"plate_dry_van": "Plate Dry Van",
"storage_van": "Storage Van",
"reefer_van": "Reefer Van",
"heated_van": "Heated Van",
"flatbed": "Flatbed",
"dropdeck": "Dropdeck"
},
"country": {
"canada": {
"title": "Canada",
"code": "CA",
"province": {
"ontario": {
"title": "Ontario",
"code": "ON"
},
"quebec": {
"title": "Quebec",
"code": "QC"
},
"britishColumbia": {
"title": "British Columbia",
"code": "BC"
},
"alberta": {
"title": "Alberta",
"code": "AB"
},
"manitoba": {
"title": "Manitoba",
"code": "MB"
},
"saskatchewan": {
"title": "Saskatchewan",
"code": "SK"
},
"novaScotia": {
"title": "Nova Scotia",
"code": "NS"
},
"newBrunswick": {
"title": "New Brunswick",
"code": "NB"
},
"newfoundlandAndLabrador": {
"title": "Newfoundland and Labrador",
"code": "NL"
},
"princeEdwardIsland": {
"title": "Prince Edward Island",
"code": "PE"
},
"northwestTerritories": {
"title": "Northwest Territories",
"code": "NT"
},
"nunavut": {
"title": "Nunavut",
"code": "NU"
},
"yukon": {
"title": "Yukon",
"code": "YT"
}
}
}
}
}

View File

@ -0,0 +1,6 @@
{
"nav": {
"organizations": "Organisations",
"reservations": "Réservations"
}
}

View File

@ -0,0 +1,239 @@
{
"appTitle": "vHub",
"fatalError": "Une erreur inattendue est survenue. Veuillez réessayer plus tard.",
"navigation": {
"admin": {
"organizations": "Organisations",
"reservations": "Réservations",
"title": "Admin"
},
"owner": {
"equipments": "Équipements",
"parkingLots": "Cours de stationnement",
"reservations": "Réservations",
"title": "Propriétaire"
},
"renter": {
"reservations": "Réservations",
"title": "Locataire"
},
"account": {
"administration": "Administration",
"organization": "Gérer mon organisation"
},
"logout": "Déconnexion",
"login": "Connexion",
"switchLanguage": {
"en": "English",
"fr": "Français"
},
"search": "Rechercher"
},
"languages": {
"english": {
"title": "Anglais",
"code": "en"
},
"french": {
"title": "Français",
"code": "fr"
}
},
"mileage": {
"KILOMETERS": "km",
"MILES": "mi"
},
"notFound": {
"goHome": "Retourner à la page daccueil",
"message": "La page que vous tentiez datteindre nest pas disponible ou nexiste pas.",
"title": "Page introuvable"
},
"reservations": {
"activities": {
"chronology": {
"application": "Heure de lappareil",
"date": "{{date, DD-MM-YYYY, H:mm}}",
"synchronization": "Synchronisé à",
"title": "Chronologie"
},
"damages": {
"date": "{{date, D MMMM YYYY, H:mm}}",
"noDamages": "Aucun dommage déclaré",
"title": "Dommages"
},
"inspection": {
"noConditionReports": "Aucun rapport de condition déclaré",
"title": "Inspection"
},
"licencePlate": {
"title": "Plaque dimmatriculation"
},
"location": {
"addressTitle": "Position du conducteur",
"error": "Impossible de résoudre ladresse. Réessayez plus tard.",
"notFound": "Aucune adresse trouvée à cet emplacement",
"notProvided": "Non fourni",
"title": "Emplacement"
},
"odometer": {
"notProdived": "Aucune information fournie.",
"title": "Odomètre"
},
"review": {
"reasons": {
"alreadyPacked": "Pas vide",
"dirty": "Intérieur sale",
"damaged": "Très endommagé",
"snow": "Couverte de neige",
"copled": "Couplée ou inaccessible",
"notCompliant": "Non conforme pour circuler",
"other": "Autre"
},
"title": "Critique"
}
},
"assignees": {
"invitationList": {
"empty": "Aucune invitation na encore été envoyée"
},
"sendInvite": {
"form": {
"actions": {
"submit": {
"caption": "Envoyer"
}
},
"errors": {
"failed": "Impossible denvoyer un SMS pour le moment, veuillez réessayer plus tard."
},
"fields": {
"driverName": {
"errors": {
"required": "Vous devez entrer un nom"
},
"label": "Nom",
"placeholder": "Jean Tremblay"
},
"phoneNumber": {
"errors": {
"invalid": "Doit respecter le format suivant: +15553219876",
"required": "Vous devez entrer un numéro de téléphone"
},
"label": "Numéro de téléphone",
"placeholder": "+15553219876"
}
}
}
}
},
"stateFilter": {
"all": "Toutes",
"current": "En cours",
"cancelled": "Annulées",
"done": "Terminées",
"upcoming": "À venir",
"late_activation": "Non activées",
"late_deactivation": "Non désactivées"
},
"statusBadge": {
"activated": "Activée le {{date, DD\/MM\/YYYY HH:mm}}",
"deactivated": "Désactivée le {{date, DD\/MM\/YYYY HH:mm}}",
"lateActivation": "Activation en retard {{date, DD\/MM\/YYYY}}",
"lateDeactivation": "Désactivation en retard {{date, DD\/MM\/YYYY}}",
"upcoming": "À venir"
}
},
"search": "Rechercher",
"searchMap": {
"departure": "Départ"
},
"timeAgo": "Il y a {{distance}}",
"errors": {
"overlap_availabilities": "Les dates sélectionnées chevauchent une autre disponibilité",
"overlap_reservations": "Les dates sélectionnées chevauchent une réservation",
"equipment_not_available": "Léquipement n'est pas disponible pour ces dates"
},
"form": {
"actions": {
"delete": {
"caption": "Supprimer"
},
"cancel": {
"caption": "Annuler"
}
}
},
"currency": {
"prefixSymbol": "",
"suffixSymbol": " $"
},
"trailerTypes": {
"dry_van": "Fourgon sec",
"plate_dry_van": "Fourgon sec plate",
"storage_van": "Fourgon d'entreposage",
"reefer_van": "Fourgon réfrigéré",
"heated_van": "Fourgon chauffé",
"flatbed": "Plateforme",
"dropdeck": "Plateforme surbaissée"
},
"country": {
"canada": {
"title": "Canada",
"code": "CA",
"province": {
"ontario": {
"title": "Ontario",
"code": "ON"
},
"quebec": {
"title": "Québec",
"code": "QC"
},
"britishColumbia": {
"title": "Colombie-Britannique",
"code": "BC"
},
"alberta": {
"title": "Alberta",
"code": "AB"
},
"manitoba": {
"title": "Manitoba",
"code": "MB"
},
"saskatchewan": {
"title": "Saskatchewan",
"code": "SK"
},
"novaScotia": {
"title": "Nouvelle-Écosse",
"code": "NS"
},
"newBrunswick": {
"title": "Nouveau-Brunswick",
"code": "NB"
},
"newfoundlandAndLabrador": {
"title": "Terre-Neuve-et-Labrador",
"code": "NL"
},
"princeEdwardIsland": {
"title": "Île-du-Prince-Édouard",
"code": "PE"
},
"northwestTerritories": {
"title": "Territoires du Nord-Ouest",
"code": "NT"
},
"nunavut": {
"title": "Nunavut",
"code": "NU"
},
"yukon": {
"title": "Yukon",
"code": "YT"
}
}
}
}
}

3641
cli/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

73
cli/package.json Normal file
View File

@ -0,0 +1,73 @@
{
"name": "accent-cli",
"version": "0.6.0",
"author": "Simon Prévost",
"description": "Accent CLI",
"bin": {
"accent": "./bin/run"
},
"dependencies": {
"@oclif/command": "1.5.6",
"@oclif/config": "1.9.0",
"@oclif/plugin-help": "2.1.4",
"@oclif/plugin-not-found": "1.2.2",
"@types/decamelize": "1.2.0",
"@types/form-data": "2.2.1",
"@types/fs-extra": "5.0.1",
"@types/glob": "5.0.35",
"@types/node-fetch": "1.6.7",
"chalk": "2.4.1",
"cli-ux": "4.9.3",
"decamelize": "2.0.0",
"form-data": "2.3.3",
"glob": "7.1.3",
"node-fetch": "2.3.0",
"tslib": "1.9.3",
"tslint-config-prettier": "1.17.0"
},
"devDependencies": {
"@oclif/dev-cli": "1.21.0",
"@oclif/test": "1.0.1",
"@oclif/tslint": "3.1.1",
"@types/chai": "4.1.2",
"@types/mocha": "5.0.0",
"@types/node": "9.6.0",
"chai": "4.1.2",
"globby": "8.0.1",
"mocha": "5.0.5",
"prettier": "1.15.3",
"ts-node": "7.0.1",
"tslint": "5.11.0",
"typescript": "3.2.2"
},
"engines": {
"node": ">=8.0.0"
},
"files": [
".oclif.manifest.json",
"/bin",
"/lib"
],
"keywords": [
"oclif"
],
"license": "MIT",
"main": "lib/index.js",
"oclif": {
"commands": "./lib/commands",
"bin": "accent",
"plugins": [
"@oclif/plugin-help",
"@oclif/plugin-not-found"
]
},
"scripts": {
"build": "rm -rf lib && tsc",
"clean": "rm -f .oclif.manifest.json",
"lint": "tslint --project tsconfig.json 'src/**/*.{ts,tsx}'",
"fix": "tslint --project tsconfig.json --fix 'src/**/*.{ts,tsx}'",
"prepublishOnly": "npm run build && oclif-dev readme && oclif-dev manifest",
"prettier": "prettier --single-quote --no-bracket-spacing --no-semi --write './src/**/*.{js,json,ts,tsx,gql}'"
},
"types": "lib/index.d.ts"
}

41
cli/src/base.ts Normal file
View File

@ -0,0 +1,41 @@
// Vendor
import Command from '@oclif/command'
import {error} from '@oclif/errors'
import chalk from 'chalk'
import cli from 'cli-ux'
// Services
import ConfigFetcher from './services/config'
import ProjectFetcher from './services/project-fetcher'
// Types
import {Project} from './types/project'
const sleep = (ms: number) =>
new Promise((resolve: () => void) => setTimeout(resolve, ms))
export default abstract class extends Command {
public projectConfig: ConfigFetcher = new ConfigFetcher()
public project?: Project
public async init() {
const config = this.projectConfig.config
if (!config.apiUrl) error('You must set an API url in your config')
if (!config.apiKey) error('You must set an API key in your config')
// Fetch project from the GraphQL API.
cli.action.start(chalk.white('Fetch config'))
await sleep(1000)
const fetcher = new ProjectFetcher()
this.project = await fetcher.fetch(config)
cli.action.stop(chalk.green('✓'))
console.log('')
}
public async refreshProject() {
const config = this.projectConfig.config
const fetcher = new ProjectFetcher()
this.project = await fetcher.fetch(config)
}
}

View File

@ -0,0 +1,55 @@
// Command
import {flags} from '@oclif/command'
import Command from '../base'
// Formatters
import ExportFormatter from '../services/formatters/project-export'
// Services
import DocumentPathsFetcher from '../services/document-paths-fetcher'
import DocumentExportFormatter from '../services/formatters/document-export'
import HookRunner from '../services/hook-runner'
// Types
import {Hooks} from '../types/document-config'
export default class Export extends Command {
public static description =
'Export files from Accent and write them to your local filesystem'
public static examples = [`$ accent export`]
public static args = []
public static flags = {
'order-by': flags.string({
default: 'index',
description: 'Will be used in the export call as the order of the keys',
options: ['index', 'key-asc']
})
}
public async run() {
const {flags} = this.parse(Export)
const documents = this.projectConfig.files()
const formatter = new DocumentExportFormatter()
// From all the documentConfigs, do the export, write to local file and log the results.
new ExportFormatter().log()
for (const document of documents) {
await new HookRunner(document).run(Hooks.beforeExport)
const targets = new DocumentPathsFetcher().fetch(this.project!, document)
await Promise.all(
targets.map(({path, language, documentPath}) => {
formatter.log(path)
return document.export(path, language, documentPath, flags)
})
)
await new HookRunner(document).run(Hooks.afterExport)
}
}
}

57
cli/src/commands/jipt.ts Normal file
View File

@ -0,0 +1,57 @@
// Command
import Command from '../base'
// Formatters
import ExportFormatter from '../services/formatters/project-export'
// Services
import DocumentJiptPathsFetcher from '../services/document-jipt-paths-fetcher'
import DocumentExportFormatter from '../services/formatters/document-export'
import HookRunner from '../services/hook-runner'
// Types
import {Hooks} from '../types/document-config'
export default class Jipt extends Command {
public static description =
'Export jipt files from Accent and write them to your local filesystem'
public static examples = [`$ accent jipt`]
public static args = [
{
description: 'The pseudo language for in-place-translation-editing',
name: 'pseudoLanguageName',
required: true
}
]
public static flags = {}
public async run() {
const {args} = this.parse(Jipt)
const documents = this.projectConfig.files()
const formatter = new DocumentExportFormatter()
// From all the documentConfigs, do the export, write to local file and log the results.
new ExportFormatter().log()
for (const document of documents) {
await new HookRunner(document).run(Hooks.beforeExport)
const targets = new DocumentJiptPathsFetcher().fetch(
this.project!,
document,
args.pseudoLanguageName
)
await Promise.all(
targets.map(({path, documentPath}) => {
formatter.log(path)
return document.exportJipt(path, documentPath)
})
)
await new HookRunner(document).run(Hooks.afterExport)
}
}
}

18
cli/src/commands/stats.ts Normal file
View File

@ -0,0 +1,18 @@
// Command
import Command from '../base'
// Services
import Formatter from '../services/formatters/project-stats'
export default class Stats extends Command {
public static description =
'Fetch stats from the API and display it beautifully'
public static examples = [`$ accent stats`]
public async run() {
const formatter = new Formatter(this.project!)
formatter.log()
}
}

149
cli/src/commands/sync.ts Normal file
View File

@ -0,0 +1,149 @@
// Vendor
import {flags} from '@oclif/command'
import {existsSync} from 'fs'
// Command
import Command from '../base'
// Formatters
import AddTranslationsFormatter from '../services/formatters/project-add-translations'
import ExportFormatter from '../services/formatters/project-export'
import SyncFormatter from '../services/formatters/project-sync'
// Services
import Document from '../services/document'
import DocumentPathsFetcher from '../services/document-paths-fetcher'
import CommitOperationFormatter from '../services/formatters/commit-operation'
import DocumentExportFormatter from '../services/formatters/document-export'
import HookRunner from '../services/hook-runner'
// Types
import {Hooks} from '../types/document-config'
export default class Sync extends Command {
public static description =
'Sync files in Accent and write them to your local filesystem'
public static examples = [`$ accent sync`]
public static args = []
public static flags = {
'add-translations': flags.boolean({
description:
'Add translations in Accent to help translators if you already have translated strings'
}),
'merge-type': flags.string({
default: 'smart',
description:
'Will be used in the add translations call as the "merge_type" param',
options: ['smart', 'passive', 'force']
}),
'order-by': flags.string({
default: 'index',
description: 'Will be used in the export call as the order of the keys',
options: ['index', 'key-asc']
}),
'sync-type': flags.string({
default: 'smart',
description: 'Will be used in the sync call as the "sync_type" param',
options: ['smart', 'passive']
}),
write: flags.boolean({
description: 'Write the file from the export _after_ the operation'
})
}
public async run() {
const {flags} = this.parse(Sync)
const documents = this.projectConfig.files()
// From all the documentConfigs, do the sync or peek operations and log the results.
new SyncFormatter().log()
for (const document of documents) {
await new HookRunner(document).run(Hooks.beforeSync)
await Promise.all(this.syncDocumentConfig(document))
await new HookRunner(document).run(Hooks.afterSync)
}
if (flags['add-translations']) {
new AddTranslationsFormatter().log()
for (const document of documents) {
await new HookRunner(document).run(Hooks.beforeAddTranslations)
await Promise.all(this.addTranslationsDocumentConfig(document))
await new HookRunner(document).run(Hooks.afterAddTranslations)
}
}
if (!flags.write) return
// After syncing the files in Accent, the list of documents could have changed.
await this.refreshProject()
const formatter = new DocumentExportFormatter()
// From all the documentConfigs, do the export, write to local file and log the results.
new ExportFormatter().log()
for (const document of documents) {
await new HookRunner(document).run(Hooks.beforeExport)
const targets = new DocumentPathsFetcher().fetch(this.project!, document)
await Promise.all(
targets.map(({path, language, documentPath}) => {
formatter.log(path)
return document.export(path, language, documentPath, flags)
})
)
await new HookRunner(document).run(Hooks.afterExport)
}
}
private syncDocumentConfig(document: Document) {
const {flags} = this.parse(Sync)
const formatter = new CommitOperationFormatter()
return document.paths.map(async path => {
const operations = await document.sync(path, flags)
if (operations.sync && !operations.peek) formatter.logSync(path)
if (operations.peek) formatter.logPeek(path, operations.peek)
return operations
})
}
private addTranslationsDocumentConfig(document: Document) {
const {flags} = this.parse(Sync)
const formatter = new CommitOperationFormatter()
const targets = new DocumentPathsFetcher()
.fetch(this.project!, document)
.filter(({language}) => language !== document.config.language)
.filter(({path}) => existsSync(path))
return targets.map(async ({path, language, documentPath}) => {
const operations = await document.addTranslations(
path,
language,
documentPath,
flags
)
if (operations.addTranslations && !operations.peek) {
formatter.logAddTranslations(path)
}
if (operations.peek) formatter.logPeek(path, operations.peek)
return operations
})
}
}

1
cli/src/index.ts Normal file
View File

@ -0,0 +1 @@
export {run} from '@oclif/command'

View File

@ -0,0 +1,41 @@
// Vendor
import {error} from '@oclif/errors'
import * as fs from 'fs-extra'
// Services
import Document from './document'
// Types
import {Config} from '../types/config'
export default class ConfigFetcher {
public readonly config: Config
constructor() {
this.config = fs.readJsonSync('accent.json')
this.config.apiKey = this.config.apiKey || process.env.ACCENT_API_KEY!
this.config.apiUrl = this.config.apiUrl || process.env.ACCENT_API_URL!
if (!this.config.apiKey) {
error(
'You must have an apiKey key in the config or the ACCENT_API_KEY environment variable'
)
}
if (!this.config.apiUrl) {
error(
'You must have an apiUrl key in the config or the ACCENT_API_URL environment variable'
)
}
if (!this.config.files) {
error('You must have at least 1 document set in your config')
}
}
public files(): Document[] {
return this.config.files.map(
documentConfig => new Document(documentConfig, this.config)
)
}
}

View File

@ -0,0 +1,26 @@
// Types
import {DocumentPath} from '../types/document-path'
import {Project} from '../types/project'
import Document from './document'
export default class DocumentJiptPathsFetcher {
public fetch(
project: Project,
document: Document,
pseudoLanguageName: string
): DocumentPath[] {
return project.documents.entries
.map(({path}) => path)
.map(path => {
const parsedTarget = document.target
.replace('%slug%', pseudoLanguageName)
.replace('%original_file_name%', path)
return {
documentPath: path,
language: pseudoLanguageName,
path: parsedTarget
}
})
}
}

View File

@ -0,0 +1,22 @@
// Types
import {DocumentPath} from '../types/document-path'
import {Project} from '../types/project'
import Document from './document'
export default class DocumentPathsFetcher {
public fetch(project: Project, document: Document): DocumentPath[] {
const languageSlugs = project.revisions.map(({language}) => language.slug)
const documentPaths = project.documents.entries.map(({path}) => path)
return languageSlugs.reduce((memo: DocumentPath[], slug) => {
documentPaths.forEach(path => {
const parsedTarget = document.target
.replace('%slug%', slug)
.replace('%original_file_name%', path)
memo.push({documentPath: path, path: parsedTarget, language: slug})
})
return memo
}, [])
}
}

View File

@ -0,0 +1,162 @@
// Vendor
import * as FormData from 'form-data'
import * as fs from 'fs-extra'
import fetch, {Response} from 'node-fetch'
import * as path from 'path'
// Services
import Tree from './tree'
// Types
import {Config} from '../types/config'
import {DocumentConfig} from '../types/document-config'
import {OperationResponse} from '../types/operation-response'
const enum OperationName {
Sync = 'sync',
AddTranslation = 'addTranslations'
}
export default class Document {
public paths: string[]
public readonly apiKey: string
public readonly apiUrl: string
public readonly config: DocumentConfig
public readonly target: string
constructor(documentConfig: DocumentConfig, config: Config) {
this.config = documentConfig
this.apiKey = config.apiKey
this.apiUrl = config.apiUrl
this.target = this.config.target
this.paths = new Tree(this.config).list()
}
public refreshPaths() {
this.paths = new Tree(this.config).list()
}
public async sync(file: string, options: any) {
const formData = new FormData()
formData.append('file', fs.createReadStream(file))
formData.append('document_path', this.parseDocumentName(file))
formData.append('document_format', this.config.format)
formData.append('language', this.config.language)
let url = `${this.apiUrl}/sync`
if (!options.write) url = `${url}/peek`
if (options['sync-type']) formData.append('sync_type', options['sync-type'])
const response = await fetch(url, {
body: formData,
headers: this.authorizationHeader(),
method: 'POST'
})
return this.handleResponse(response, options, OperationName.Sync)
}
public async addTranslations(
file: string,
language: string,
documentPath: string,
options: any
) {
const formData = new FormData()
formData.append('file', fs.createReadStream(file))
formData.append('document_path', documentPath)
formData.append('document_format', this.config.format)
formData.append('language', language)
let url = `${this.apiUrl}/add-translations`
if (!options.write) url = `${url}/peek`
if (options['merge-type']) {
formData.append('merge_type', options['merge-type'])
}
const response = await fetch(url, {
body: formData,
headers: this.authorizationHeader(),
method: 'POST'
})
return this.handleResponse(response, options, OperationName.AddTranslation)
}
public async export(
file: string,
language: string,
documentPath: string,
options: any
) {
language = language || this.config.language
const query = [
['document_path', documentPath],
['document_format', this.config.format],
['order_by', options['order-by']],
['language', language]
]
.map(([name, value]) => `${name}=${value}`)
.join('&')
const url = `${this.apiUrl}/export?${query}`
const response = await fetch(url, {
headers: this.authorizationHeader()
})
return this.writeResponseToFile(response, file)
}
public async exportJipt(file: string, documentPath: string) {
const query = [
['document_path', documentPath],
['document_format', this.config.format]
]
.map(([name, value]) => `${name}=${value}`)
.join('&')
const url = `${this.apiUrl}/jipt-export?${query}`
const response = await fetch(url, {
headers: this.authorizationHeader()
})
return this.writeResponseToFile(response, file)
}
private authorizationHeader() {
return {authorization: `Bearer ${this.apiKey}`}
}
private parseDocumentName(file: string): string {
return path.basename(file).replace(path.extname(file), '')
}
private writeResponseToFile(response: Response, file: string) {
return new Promise((resolve, reject) => {
const fileStream = fs.createWriteStream(file, {autoClose: true})
response.body.pipe(fileStream)
response.body.on('error', reject)
fileStream.on('finish', resolve)
})
}
private async handleResponse(
response: Response,
options: any,
operationName: OperationName
): Promise<OperationResponse> {
if (options.write) {
if (response.status >= 400) {
return {[operationName]: {success: false}, peek: false}
}
return {[operationName]: {success: true}, peek: false}
} else {
const {data} = await response.json()
return {peek: data, [operationName]: {success: true}}
}
}
}

View File

@ -0,0 +1,55 @@
// Vendor
import chalk from 'chalk'
// Types
import {PeekOperation} from '../../types/operation'
// Constants
const MASTER_ONLY_ACTIONS = ['new', 'renew', 'remove']
export default class CommitOperationFormatter {
public logSync(path: string) {
console.log(' ', chalk.white(path))
console.log(' ', chalk.green('✓ Successfully synced the files in Accent'))
console.log('')
}
public logAddTranslations(path: string) {
console.log(' ', chalk.white(path))
console.log(' ', chalk.green('✓ Successfully add translations in Accent'))
console.log('')
}
public logPeek(path: string, operations: PeekOperation) {
console.log(' ', chalk.white(path))
if (!Object.keys(operations.stats).length) {
console.log(' ', chalk.gray('~~ No changes for this file ~~'))
}
Object.entries(operations.stats).map((stat, index) => {
let actions = Object.entries(stat[1])
if (index > 0) {
actions = actions.filter(
([action]) => !MASTER_ONLY_ACTIONS.includes(action)
)
}
actions.map(([action, name]) => {
console.log(
' ',
chalk.bold(this.formatAction(action)),
':',
chalk.bold.white(name)
)
})
})
console.log('')
}
private formatAction(action: string) {
const capitalized = action.charAt(0).toUpperCase() + action.slice(1)
return capitalized.replace(/_/g, ' ')
}
}

View File

@ -0,0 +1,13 @@
// Vendor
import chalk from 'chalk'
export default class DocumentExportFormatter {
public log(path: string) {
console.log(' ', chalk.white(path))
console.log(
' ',
chalk.green('✓ Successfully write the locale files from Accent')
)
console.log('')
}
}

View File

@ -0,0 +1,17 @@
// Vendor
import chalk from 'chalk'
import * as decamelize from 'decamelize'
const capitalizeFirstLetter = (str: string) =>
str.charAt(0).toUpperCase() + str.slice(1)
export default class HookRunnerFomatter {
public log(name: string, commands: string[]) {
const operation = capitalizeFirstLetter(decamelize(name, ' '))
console.log(chalk.yellow('➤ '), chalk.bold(chalk.yellow(`${operation}:`)))
commands.forEach(command => {
console.log(' ', chalk.yellow(command))
})
console.log('')
}
}

View File

@ -0,0 +1,10 @@
// Vendor
import chalk from 'chalk'
export default class ProjectAddTranslationsFormatter {
public log() {
console.log(chalk.magenta('Adding translations paths'))
console.log('')
}
}

View File

@ -0,0 +1,10 @@
// Vendor
import chalk from 'chalk'
export default class ProjectExportFormatter {
public log() {
console.log(chalk.magenta('Writing files'))
console.log('')
}
}

View File

@ -0,0 +1,84 @@
// Vendor
import chalk from 'chalk'
// Types
import {Document, Project, Revision} from '../../types/project'
export default class ProjectStatsFormatter {
private readonly project: Project
constructor(project: Project) {
this.project = project
}
public log() {
const translationsCount = this.project.revisions.reduce(
(memo, revision: Revision) => memo + revision.translationsCount,
0
)
const conflictsCount = this.project.revisions.reduce(
(memo, revision: Revision) => memo + revision.conflictsCount,
0
)
const reviewedCount = this.project.revisions.reduce(
(memo, revision: Revision) => memo + revision.reviewedCount,
0
)
console.log(chalk.magenta('Last synced'))
console.log(' ', chalk.white.bold(this.project.lastSyncedAt))
console.log('')
console.log(chalk.magenta('Master language'))
console.log(
' ',
chalk.white.bold(this.project.language.name) +
' ' +
this.project.language.slug
)
console.log('')
if (this.project.revisions.length > 1) {
console.log(
chalk.magenta(`Translations (${this.project.revisions.length - 1})`)
)
this.project.revisions.forEach((revision: Revision) => {
if (this.project.language.id !== revision.language.id) {
console.log(
' ',
chalk.white.bold(revision.language.name) +
' ' +
revision.language.slug
)
console.log('')
}
})
}
console.log(chalk.magenta('Documents'))
this.project.documents.entries.forEach((document: Document) => {
console.log(
' ',
chalk.gray('Format:'),
chalk.white.bold(document.format)
)
console.log(' ', chalk.gray('Path:'), chalk.white.bold(document.path))
console.log('')
})
console.log(chalk.magenta('Strings'))
console.log(
' ',
chalk.white('# Strings:'),
chalk.white(`${translationsCount}`)
)
console.log(
' ',
chalk.green('✓ Reviewed:'),
chalk.green(`${reviewedCount}`)
)
console.log(' ', chalk.red('× In review:'), chalk.red(`${conflictsCount}`))
}
}

View File

@ -0,0 +1,10 @@
// Vendor
import chalk from 'chalk'
export default class ProjectSyncFormatter {
public log() {
console.log(chalk.magenta('Syncing sources'))
console.log('')
}
}

View File

@ -0,0 +1,32 @@
// Vendor
import {execSync} from 'child_process'
// Formatters
import Formatter from './formatters/hook-runner'
// Types
import {HookConfig, Hooks} from '../types/document-config'
import Document from './document'
export default class HookRunner {
public readonly hooks?: HookConfig
private readonly document: Document
constructor(document: Document) {
this.document = document
this.hooks = document.config.hooks
}
public async run(name: Hooks) {
if (!this.hooks) return null
const hooks = this.hooks[name]
if (hooks) {
new Formatter().log(name, hooks)
hooks.forEach(execSync)
}
return this.document.refreshPaths()
}
}

View File

@ -0,0 +1,65 @@
// Vendor
import {error} from '@oclif/errors'
import fetch from 'node-fetch'
// Types
import {Config} from '../types/config'
import {Project} from '../types/project'
export default class ProjectFetcher {
public async fetch(config: Config): Promise<Project> {
const response = await this.graphql(config)
const data = await response.json()
if (!data.data) {
error(`Can not find the project for the key: ${config.apiKey}`)
}
return data.data && data.data.viewer.project
}
private graphql(config: Config) {
const query = `query ProjectDetails($project_id: ID!) {
viewer {
project(id: $project_id) {
id
name
lastSyncedAt
language {
id
name
slug
}
documents {
entries {
id
path
format
}
}
revisions {
id
translationsCount
conflictsCount
reviewedCount
language {
id
name
slug
}
}
}
}
}`
return fetch(`${config.apiUrl}/graphql`, {
body: JSON.stringify({query}),
headers: {
'Content-Type': 'application/json',
authorization: `Bearer ${config.apiKey}`
},
method: 'POST'
})
}
}

17
cli/src/services/tree.ts Normal file
View File

@ -0,0 +1,17 @@
// Vendor
import * as glob from 'glob'
// Types
import {DocumentConfig} from '../types/document-config'
export default class Tree {
private readonly document: DocumentConfig
constructor(document: DocumentConfig) {
this.document = document
}
public list(): string[] {
return glob.sync(this.document.source, {})
}
}

8
cli/src/types/config.ts Normal file
View File

@ -0,0 +1,8 @@
// Types
import {DocumentConfig} from './document-config'
export interface Config {
apiUrl: string
apiKey: string
files: DocumentConfig[]
}

View File

@ -0,0 +1,26 @@
export enum Hooks {
beforeAddTranslations = 'beforeAddTranslations',
afterAddTranslations = 'afterAddTranslations',
beforeExport = 'beforeExport',
afterExport = 'afterExport',
beforeSync = 'beforeSync',
afterSync = 'afterSync'
}
export interface HookConfig {
[Hooks.beforeAddTranslations]: string[]
[Hooks.afterAddTranslations]: string[]
[Hooks.beforeExport]: string[]
[Hooks.afterExport]: string[]
[Hooks.beforeSync]: string[]
[Hooks.afterSync]: string[]
}
export interface DocumentConfig {
name: string
language: string
format: string
source: string
target: string
hooks?: HookConfig
}

View File

@ -0,0 +1,5 @@
export interface DocumentPath {
path: string
language: string
documentPath: string
}

View File

@ -0,0 +1,4 @@
export interface OperationResponse {
peek: any
[x: string]: any
}

View File

@ -0,0 +1,4 @@
export interface PeekOperation {
operations: any[]
stats: any
}

31
cli/src/types/project.ts Normal file
View File

@ -0,0 +1,31 @@
export interface Language {
id: string
name: string
slug: string
}
export interface Revision {
id: string
language: Language
translationsCount: number
conflictsCount: number
reviewedCount: number
}
export interface Document {
path: string
format: string
}
export interface PaginatedDocuments {
entries: Document[]
}
export interface Project {
id: string
name: string
lastSyncedAt: string
language: Language
revisions: Revision[]
documents: PaginatedDocuments
}

29
cli/tsconfig.json Normal file
View File

@ -0,0 +1,29 @@
{
"compilerOptions": {
"allowJs": false,
"allowUnreachableCode": false,
"allowSyntheticDefaultImports": true,
"declaration": true,
"forceConsistentCasingInFileNames": true,
"importHelpers": true,
"module": "commonjs",
"outDir": "./lib",
"pretty": true,
"noFallthroughCasesInSwitch": true,
"noEmit": true,
"noImplicitReturns": true,
"noUnusedLocals": true,
"rootDirs": [
"./src"
],
"skipLibCheck": true,
"strict": true,
"target": "es2017"
},
"include": [
"./src/**/*"
],
"exclude": [
"node_modules/**/*"
]
}

21
cli/tslint.json Normal file
View File

@ -0,0 +1,21 @@
{
"extends": ["@oclif/tslint", "tslint:latest", "tslint-config-prettier"],
"rules": {
"no-implicit-dependencies": false,
"no-submodule-imports": false,
"no-console": false,
"variable-name": [
true,
"ban-keywords",
"check-format",
"allow-leading-underscore",
"allow-pascal-case"
],
"prefer-const": true,
"no-empty-interface": false,
"interface-name": false,
"no-shadowed-variable": false,
"curly": [true, "ignore-same-line"],
"no-empty": false
}
}