Refactor cli config to simplify language and document path parsi… (#127)

* Refactor cli config to simplify language and document path parsing

* Run format

* Remove namePattern inferred by cli in ember multifiles example

* Update cli/src/services/document.ts

Co-Authored-By: Rémi Prévost <remi@exomel.com>

* Add changelog

* Run format

* Run lint
This commit is contained in:
Simon Prévost 2019-10-31 07:33:36 -04:00 committed by GitHub
parent 1068f10e5c
commit 5be2645114
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 811 additions and 685 deletions

17
cli/CHANGELOG.md Normal file
View File

@ -0,0 +1,17 @@
# Changelog
## 0.8.0
### Breaking changes
- `language` key is no longer supported in `files` items since the `language` is always the master language.
### Soft Deprecations
- Use `%document_path%` placeholder instead of `%original_file_name%` as it better reflect Accent nomenclature.
### Features
- `namePattern` key is used to force either `parentDirectory`, `fullDirectory` or `file` config to use as `documentPath` sent to Accent.
- Directories are now created on-the-fly when using the `--write` flag.
- New useful logs

View File

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

View File

@ -0,0 +1,3 @@
{
"key": "c"
}

View File

@ -0,0 +1,3 @@
{
"key": "value"
}

View File

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

View File

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

View File

@ -1,24 +1,22 @@
{
"files": [
{
"language": "en",
"format": "json",
"source": "aigu/*.json",
"target": "aigu/%original_file_name%-%slug%.json",
"target": "aigu/%document_path%-%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"
"aigu rails-export --locale=en --output-file=aigu/aigu-is-awesome.json"
],
"beforeAddTranslations": [
"aigu rails-export --locale=fr --output-file=aigu/aigu-is-awesome-fr.json >/dev/null 2>&1"
"aigu rails-export --locale=fr --output-file=aigu/aigu-is-awesome-fr.json"
],
"beforeExport": ["rm -rf aigu", "mkdir -p aigu"],
"beforeExport": ["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"
"aigu rails-import --locale=en --input-file=aigu/aigu-is-awesome-en.json",
"aigu rails-import --locale=fr --input-file=aigu/aigu-is-awesome-fr.json"
]
}
}

View File

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

1239
cli/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -16,6 +16,7 @@
"@types/fs-extra": "5.0.1",
"@types/glob": "5.0.35",
"@types/node-fetch": "1.6.7",
"mkdirp": "0.5.1",
"chalk": "2.4.1",
"cli-ux": "4.9.3",
"decamelize": "2.0.0",
@ -30,6 +31,7 @@
"@oclif/test": "1.0.1",
"@oclif/tslint": "3.1.1",
"@types/chai": "4.1.2",
"@types/mkdirp": "0.5.2",
"@types/mocha": "5.0.0",
"@types/node": "9.6.0",
"chai": "4.1.2",

View File

@ -28,6 +28,7 @@ export default abstract class extends Command {
await sleep(1000);
const fetcher = new ProjectFetcher();
this.project = await fetcher.fetch(config);
if (!this.project) error('Unable to fetch project');
cli.action.stop(chalk.green('✓'));
console.log('');
}

View File

@ -44,9 +44,11 @@ export default class Export extends Command {
await Promise.all(
targets.map(({path, language, documentPath}) => {
formatter.log(path);
const localFile = document.fetchLocalFile(documentPath, path);
if (!localFile) return new Promise(resolve => resolve());
formatter.log(localFile);
return document.export(path, language, documentPath, flags);
return document.export(localFile, language, documentPath, flags);
})
);

View File

@ -56,11 +56,10 @@ export default class Sync extends Command {
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();
new SyncFormatter().log(this.project!);
for (const document of documents) {
await new HookRunner(document).run(Hooks.beforeSync);
@ -72,8 +71,8 @@ export default class Sync extends Command {
// After syncing (and writing) the files in Accent, the list of documents could have changed.
if (flags.write) await this.refreshProject();
if (flags['add-translations']) {
new AddTranslationsFormatter().log();
if (this.project!.revisions.length > 1 && flags['add-translations']) {
new AddTranslationsFormatter().log(this.project!);
for (const document of documents) {
await new HookRunner(document).run(Hooks.beforeAddTranslations);
@ -98,9 +97,12 @@ export default class Sync extends Command {
await Promise.all(
targets.map(({path, language, documentPath}) => {
formatter.log(path);
const localFile = document.fetchLocalFile(documentPath, path);
if (!localFile) return new Promise(resolve => resolve());
return document.export(path, language, documentPath, flags);
formatter.log(localFile);
return document.export(localFile, language, documentPath, flags);
})
);
@ -113,7 +115,7 @@ export default class Sync extends Command {
const formatter = new CommitOperationFormatter();
return document.paths.map(async path => {
const operations = await document.sync(path, flags);
const operations = await document.sync(this.project!, path, flags);
if (operations.sync && !operations.peek) formatter.logSync(path);
if (operations.peek) formatter.logPeek(path, operations.peek);
@ -125,13 +127,22 @@ export default class Sync extends Command {
private addTranslationsDocumentConfig(document: Document) {
const {flags} = this.parse(Sync);
const formatter = new CommitOperationFormatter();
const masterLanguage = this.project!.language.slug;
const targets = new DocumentPathsFetcher()
.fetch(this.project!, document)
.filter(({language}) => language !== document.config.language)
.filter(({path}) => existsSync(path));
.filter(({language}) => language !== masterLanguage);
return targets.map(async ({path, language, documentPath}) => {
const existingTargets = targets.filter(({path}) => existsSync(path));
if (existingTargets.length === 0) {
targets.forEach(({path}) => formatter.logEmptyExistingTarget(path));
}
if (targets.length === 0) {
formatter.logEmptyTarget(document.config.source);
}
return existingTargets.map(async ({path, language, documentPath}) => {
const operations = await document.addTranslations(
path,
language,

View File

@ -12,7 +12,8 @@ export default class DocumentPathsFetcher {
documentPaths.forEach(path => {
const parsedTarget = document.target
.replace('%slug%', slug)
.replace('%original_file_name%', path);
.replace('%original_file_name%', path)
.replace('%document_path%', path);
memo.push({documentPath: path, path: parsedTarget, language: slug});
});

View File

@ -1,6 +1,7 @@
// Vendor
import * as FormData from 'form-data';
import * as fs from 'fs-extra';
import * as mkdirp from 'mkdirp';
import fetch, {Response} from 'node-fetch';
import * as path from 'path';
@ -9,8 +10,9 @@ import Tree from './tree';
// Types
import {Config} from '../types/config';
import {DocumentConfig} from '../types/document-config';
import {DocumentConfig, NamePattern} from '../types/document-config';
import {OperationResponse} from '../types/operation-response';
import {Project} from '../types/project';
const enum OperationName {
Sync = 'sync',
@ -25,7 +27,7 @@ export default class Document {
readonly target: string;
constructor(documentConfig: DocumentConfig, config: Config) {
this.config = documentConfig;
this.config = this.resolveNamePattern(documentConfig);
this.apiKey = config.apiKey;
this.apiUrl = config.apiUrl;
this.target = this.config.target;
@ -36,12 +38,16 @@ export default class Document {
this.paths = new Tree(this.config).list();
}
async sync(file: string, options: any) {
async sync(project: Project, file: string, options: any) {
const masterLanguage = project!.language.slug;
const formData = new FormData();
formData.append('file', fs.createReadStream(file));
formData.append('document_path', this.parseDocumentName(file));
formData.append(
'document_path',
this.parseDocumentName(file, this.config.namePattern)
);
formData.append('document_format', this.config.format);
formData.append('language', this.config.language);
formData.append('language', masterLanguage);
let url = `${this.apiUrl}/sync`;
if (!options.write) url = `${url}/peek`;
@ -86,24 +92,32 @@ export default class Document {
return this.handleResponse(response, options, OperationName.AddTranslation);
}
fetchLocalFile(documentPath: string, localPath: string) {
return this.paths.reduce((memo: string | null, path: string) => {
if (
this.parseDocumentName(path, this.config.namePattern) === documentPath
) {
return localPath;
} else {
return memo;
}
}, null);
}
async export(
file: string,
language: string,
documentPath: string,
options: any
) {
const exportLanguage = language || this.config.language;
const query = [
['document_path', documentPath],
['document_format', this.config.format],
['order_by', options['order-by']],
['language', exportLanguage]
]
.map(([name, value]) => `${name}=${value}`)
.join('&');
['language', language]
];
const url = `${this.apiUrl}/export?${query}`;
const url = `${this.apiUrl}/export?${this.encodeQuery(query)}`;
const response = await fetch(url, {
headers: this.authorizationHeader()
});
@ -115,11 +129,9 @@ export default class Document {
const query = [
['document_path', documentPath],
['document_format', this.config.format]
]
.map(([name, value]) => `${name}=${value}`)
.join('&');
];
const url = `${this.apiUrl}/jipt-export?${query}`;
const url = `${this.apiUrl}/jipt-export?${this.encodeQuery(query)}`;
const response = await fetch(url, {
headers: this.authorizationHeader()
});
@ -127,16 +139,43 @@ export default class Document {
return this.writeResponseToFile(response, file);
}
private encodeQuery(params: string[][]) {
return params
.map(([name, value]) => `${name}=${encodeURIComponent(value)}`)
.join('&');
}
private authorizationHeader() {
return {authorization: `Bearer ${this.apiKey}`};
}
private parseDocumentName(file: string): string {
private resolveNamePattern(config: DocumentConfig) {
if (config.namePattern) return config;
const pattern = config.target.match(/\%slug\%\//)
? NamePattern.file
: NamePattern.parentDirectory;
config.namePattern = pattern;
return config;
}
private parseDocumentName(file: string, pattern?: NamePattern): string {
if (pattern === NamePattern.parentDirectory) {
return path.basename(path.dirname(file));
}
if (pattern === NamePattern.fullDirectory) {
return path.dirname(file);
}
return path.basename(file).replace(path.extname(file), '');
}
private writeResponseToFile(response: Response, file: string) {
return new Promise((resolve, reject) => {
mkdirp.sync(path.dirname(file));
const fileStream = fs.createWriteStream(file, {autoClose: true});
response.body.pipe(fileStream);
response.body.on('error', reject);

View File

@ -10,13 +10,25 @@ const MASTER_ONLY_ACTIONS = ['new', 'renew', 'remove'];
export default class CommitOperationFormatter {
logSync(path: string) {
console.log(' ', chalk.white(path));
console.log(' ', chalk.green('✓ Successfully synced the files in Accent'));
console.log(' ', chalk.green('↑ Successfully synced in Accent'));
console.log('');
}
logAddTranslations(path: string) {
console.log(' ', chalk.white(path));
console.log(' ', chalk.green('✓ Successfully add translations in Accent'));
console.log(' ', chalk.green('↑ Successfully added translations'));
console.log('');
}
logEmptyExistingTarget(path: string) {
console.log(' ', chalk.white(path));
console.log(' ', chalk.gray('~~ No local file ~~'));
console.log('');
}
logEmptyTarget(path: string) {
console.log(' ', chalk.bold('Source:'), chalk.white(path));
console.log(' ', chalk.gray('~~ Translations will be added on write ~~'));
console.log('');
}

View File

@ -6,7 +6,7 @@ export default class DocumentExportFormatter {
console.log(' ', chalk.white(path));
console.log(
' ',
chalk.green('✓ Successfully write the locale files from Accent')
chalk.green('↓ Successfully wrote local file from Accent')
);
console.log('');
}

View File

@ -1,9 +1,17 @@
// Vendor
import chalk from 'chalk';
// Types
import {Project} from '../../types/project';
export default class ProjectAddTranslationsFormatter {
log() {
console.log(chalk.magenta('Adding translations paths'));
log(project: Project) {
const languages = project.revisions
.filter(revision => revision.language.slug !== project.language.slug)
.map(revision => revision.language.slug)
.join(', ');
console.log(chalk.magenta('Adding translations paths', `(${languages})`));
console.log('');
}

View File

@ -25,8 +25,16 @@ export default class ProjectStatsFormatter {
0
);
console.log(chalk.magenta('Name'));
console.log(' ', chalk.white.bold(this.project.name));
console.log('');
console.log(chalk.magenta('Last synced'));
console.log(' ', chalk.white.bold(this.project.lastSyncedAt));
if (this.project.lastSyncedAt) {
console.log(' ', chalk.white.bold(this.project.lastSyncedAt));
} else {
console.log(' ', chalk.gray.bold('~~ Never synced ~~'));
}
console.log('');
@ -57,16 +65,18 @@ export default class ProjectStatsFormatter {
});
}
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('');
});
if (this.project.documents.entries.length !== 0) {
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(

View File

@ -1,9 +1,12 @@
// Vendor
import chalk from 'chalk';
// Types
import {Project} from '../../types/project';
export default class ProjectSyncFormatter {
log() {
console.log(chalk.magenta('Syncing sources'));
log(project: Project) {
console.log(chalk.magenta('Syncing sources', `(${project.language.slug})`));
console.log('');
}

View File

@ -31,6 +31,7 @@ export default class ProjectFetcher {
name
slug
}
documents {
entries {
id
@ -38,8 +39,10 @@ export default class ProjectFetcher {
format
}
}
revisions {
id
isMaster
translationsCount
conflictsCount
reviewedCount

View File

@ -7,6 +7,12 @@ export enum Hooks {
afterSync = 'afterSync'
}
export enum NamePattern {
file = 'file',
parentDirectory = 'parentDirectory',
fullDirectory = 'fullDirectory'
}
export interface HookConfig {
[Hooks.beforeAddTranslations]: string[];
[Hooks.afterAddTranslations]: string[];
@ -17,10 +23,9 @@ export interface HookConfig {
}
export interface DocumentConfig {
name: string;
language: string;
format: string;
source: string;
target: string;
namePattern?: NamePattern;
hooks?: HookConfig;
}

View File

@ -7,6 +7,7 @@ export interface Language {
export interface Revision {
id: string;
language: Language;
isMaster: boolean;
translationsCount: number;
conflictsCount: number;
reviewedCount: number;

View File

@ -3,7 +3,7 @@ import RSVP from 'rsvp';
import searchLanguagesQuery from 'accent-webapp/queries/languages-search';
const MINIMUM_TERM_LENGTH = 3;
const MINIMUM_TERM_LENGTH = 2;
export default Service.extend({
apollo: service('apollo'),