mirror of
https://github.com/mirego/accent.git
synced 2024-09-11 10:55:28 +03:00
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:
parent
1068f10e5c
commit
5be2645114
17
cli/CHANGELOG.md
Normal file
17
cli/CHANGELOG.md
Normal 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
|
9
cli/examples/ember-multifiles/accent.json
Normal file
9
cli/examples/ember-multifiles/accent.json
Normal file
@ -0,0 +1,9 @@
|
||||
{
|
||||
"files": [
|
||||
{
|
||||
"format": "json",
|
||||
"source": "translations/**/en.json",
|
||||
"target": "translations/%document_path%/%slug%.json"
|
||||
}
|
||||
]
|
||||
}
|
3
cli/examples/ember-multifiles/translations/admin/en.json
Normal file
3
cli/examples/ember-multifiles/translations/admin/en.json
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"key": "c"
|
||||
}
|
3
cli/examples/ember-multifiles/translations/login/en.json
Normal file
3
cli/examples/ember-multifiles/translations/login/en.json
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"key": "value"
|
||||
}
|
@ -1,9 +1,8 @@
|
||||
{
|
||||
"files": [
|
||||
{
|
||||
"language": "en",
|
||||
"format": "json",
|
||||
"source": "translations/en.json",
|
||||
"source": "translations/fr.json",
|
||||
"target": "translations/%slug%.json"
|
||||
}
|
||||
]
|
||||
|
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@ -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"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
@ -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
1239
cli/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -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",
|
||||
|
@ -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('');
|
||||
}
|
||||
|
@ -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);
|
||||
})
|
||||
);
|
||||
|
||||
|
@ -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,
|
||||
|
@ -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});
|
||||
});
|
||||
|
@ -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);
|
||||
|
@ -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('');
|
||||
}
|
||||
|
||||
|
@ -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('');
|
||||
}
|
||||
|
@ -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('');
|
||||
}
|
||||
|
@ -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(
|
||||
|
@ -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('');
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -7,6 +7,7 @@ export interface Language {
|
||||
export interface Revision {
|
||||
id: string;
|
||||
language: Language;
|
||||
isMaster: boolean;
|
||||
translationsCount: number;
|
||||
conflictsCount: number;
|
||||
reviewedCount: number;
|
||||
|
@ -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'),
|
||||
|
Loading…
Reference in New Issue
Block a user