Skip to content

Commit c8644d2

Browse files
committed
Add translation support. arcli i18n command to extract gettext __ and _n string literals.
1 parent e0b75ff commit c8644d2

20 files changed

+571
-3
lines changed

.core/.cli/commands/i18n/actions.js

+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
const path = require('path');
2+
const chalk = require('chalk');
3+
const fs = require('fs-extra');
4+
const _ = require('underscore');
5+
const op = require('object-path');
6+
const ShowConfigCli = require('gettext-extract');
7+
8+
module.exports = spinner => {
9+
const message = text => {
10+
if (spinner) {
11+
spinner.text = text;
12+
}
13+
};
14+
15+
return {
16+
generate: async ({ action, params, props }) => {
17+
const generator = new ShowConfigCli([]);
18+
message(`Generating ${chalk.cyan('POT file')}...`);
19+
try {
20+
generator.run();
21+
} catch (error) {
22+
console.log(error);
23+
}
24+
return Promise.resolve({ action, status: 200 });
25+
},
26+
};
27+
};

.core/.cli/commands/i18n/generator.js

+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
const ora = require('ora');
2+
const ActionSequence = require('action-sequence');
3+
4+
module.exports = ({ params, props }) => {
5+
const spinner = ora({
6+
spinner: 'dots',
7+
color: 'cyan',
8+
});
9+
10+
spinner.start();
11+
12+
const actions = require('./actions')(spinner);
13+
14+
return ActionSequence({
15+
actions,
16+
options: { params, props },
17+
})
18+
.then(success => {
19+
spinner.succeed('complete!');
20+
return success;
21+
})
22+
.catch(error => {
23+
spinner.fail('error!');
24+
return error;
25+
});
26+
};

.core/.cli/commands/i18n/index.js

+174
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
/**
2+
* -----------------------------------------------------------------------------
3+
* Imports
4+
* -----------------------------------------------------------------------------
5+
*/
6+
7+
const chalk = require('chalk');
8+
const generator = require('./generator');
9+
const prettier = require('prettier');
10+
const path = require('path');
11+
const op = require('object-path');
12+
const mod = path.dirname(require.main.filename);
13+
const { error, message } = require(`${mod}/lib/messenger`);
14+
15+
/**
16+
* NAME String
17+
* @description Constant defined as the command name. Value passed to the commander.command() function.
18+
* @example $ arcli docs
19+
* @see https://www.npmjs.com/package/commander#command-specific-options
20+
* @since 2.0.0
21+
*/
22+
const NAME = 'i18n';
23+
24+
/**
25+
* DESC String
26+
* @description Constant defined as the command description. Value passed to
27+
* the commander.desc() function. This string is also used in the --help flag output.
28+
* @see https://www.npmjs.com/package/commander#automated---help
29+
* @since 2.0.0
30+
*/
31+
const DESC = 'Generate POT file.';
32+
33+
/**
34+
* CANCELED String
35+
* @description Message sent when the command is canceled
36+
* @since 2.0.0
37+
*/
38+
const CANCELED = 'Action canceled!';
39+
40+
/**
41+
* conform(input:Object) Function
42+
* @description Reduces the input object.
43+
* @param input Object The key value pairs to reduce.
44+
* @since 2.0.0
45+
*/
46+
const CONFORM = ({ input, props }) =>
47+
Object.keys(input).reduce((obj, key) => {
48+
const { cwd } = props;
49+
let val = input[key];
50+
switch (key) {
51+
default:
52+
obj[key] = val;
53+
break;
54+
}
55+
56+
return obj;
57+
}, {});
58+
59+
/**
60+
* HELP Function
61+
* @description Function called in the commander.on('--help', callback) callback.
62+
* @see https://www.npmjs.com/package/commander#automated---help
63+
* @since 2.0.0
64+
*/
65+
const HELP = () =>
66+
console.log(`
67+
Example:
68+
$ arcli i18n -h
69+
`);
70+
71+
/**
72+
* FLAGS
73+
* @description Array of flags passed from the commander options.
74+
* @since 2.0.18
75+
*/
76+
const FLAGS = [];
77+
78+
/**
79+
* FLAGS_TO_PARAMS Function
80+
* @description Create an object used by the prompt.override property.
81+
* @since 2.0.18
82+
*/
83+
const FLAGS_TO_PARAMS = ({ opt = {} }) =>
84+
FLAGS.reduce((obj, key) => {
85+
let val = opt[key];
86+
val = typeof val === 'function' ? undefined : val;
87+
88+
if (val) {
89+
obj[key] = val;
90+
}
91+
92+
return obj;
93+
}, {});
94+
95+
/**
96+
* SCHEMA Function
97+
* @description used to describe the input for the prompt function.
98+
* @see https://www.npmjs.com/package/prompt
99+
* @since 2.0.0
100+
*/
101+
const SCHEMA = ({ props }) => {
102+
const { prompt } = props;
103+
104+
return {
105+
properties: {},
106+
};
107+
};
108+
109+
/**
110+
* ACTION Function
111+
* @description Function used as the commander.action() callback.
112+
* @see https://www.npmjs.com/package/commander
113+
* @param opt Object The commander options passed into the function.
114+
* @param props Object The CLI props passed from the calling class `orcli.js`.
115+
* @since 2.0.0
116+
*/
117+
const ACTION = ({ opt, props }) => {
118+
console.log('');
119+
120+
const { cwd, prompt } = props;
121+
const schema = SCHEMA({ props });
122+
const ovr = FLAGS_TO_PARAMS({ opt });
123+
124+
prompt.override = ovr;
125+
prompt.start();
126+
127+
return new Promise((resolve, reject) => {
128+
prompt.get(schema, (err, input = {}) => {
129+
if (err) {
130+
prompt.stop();
131+
reject(`${NAME} ${err.message}`);
132+
return;
133+
}
134+
135+
input = { ...ovr, ...input };
136+
137+
resolve(CONFORM({ input, props }));
138+
});
139+
})
140+
.then(params => {
141+
console.log('');
142+
return generator({ params, props });
143+
})
144+
.then(results => {
145+
console.log('');
146+
})
147+
.catch(err => {
148+
prompt.stop();
149+
message(op.get(err, 'message', CANCELED));
150+
});
151+
};
152+
153+
/**
154+
* COMMAND Function
155+
* @description Function that executes program.command()
156+
*/
157+
const COMMAND = ({ program, props }) =>
158+
program
159+
.command(NAME)
160+
.description(DESC)
161+
.action(opt => ACTION({ opt, props }))
162+
.on('--help', HELP);
163+
164+
/**
165+
* Module Constructor
166+
* @description Internal constructor of the module that is being exported.
167+
* @param program Class Commander.program reference.
168+
* @param props Object The CLI props passed from the calling class `arcli.js`.
169+
* @since 2.0.0
170+
*/
171+
module.exports = {
172+
COMMAND,
173+
NAME,
174+
};

.core/babel.config.js

+1
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ const alias = {
3535
'reactium-core': './.core',
3636
dependencies: './.core/dependencies',
3737
toolkit: './src/app/toolkit',
38+
'reactium-translations': './src/reactium-translations',
3839
};
3940

4041
const env = {

.core/easy-connect.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
export * from 'reactium-core/sdk/react/redux';
1+
export * from 'reactium-core/sdk/named/redux';
22

33
console.log(
44
'%cImporting from `reactium-core/easy-connect` is deprecated. Import from `reactium-core/sdk` instead.',

.core/reactium-config.js

+12
Original file line numberDiff line numberDiff line change
@@ -302,6 +302,18 @@ module.exports = {
302302
destination: '/jest.config.js',
303303
source: '/tmp/update/jest.config.js',
304304
},
305+
{
306+
overwrite: false,
307+
version: '>=3.1.0',
308+
destination: '/.gettext.json',
309+
source: '/tmp/update/.gettext.json',
310+
},
311+
{
312+
overwrite: false,
313+
version: '>=3.1.0',
314+
destination: '/src/reactium-translations',
315+
source: '/tmp/update/src/reactium-translations',
316+
},
305317
],
306318
remove: [],
307319
},

.core/sdk/i18n/index.js

+60
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import Hook from '../hook';
2+
import Jed from 'jed';
3+
4+
class i18n {
5+
locale = 'en_US';
6+
7+
constructor() {
8+
this.setDefaultLocale();
9+
}
10+
11+
setDefaultLocale = async () => {
12+
if (typeof window === 'undefined') this.locale = 'en_US';
13+
else {
14+
const langRaw =
15+
window.navigator.userLanguage || window.navigator.language;
16+
const [lang, location] = langRaw.replace('-', '_').split('_');
17+
this.locale = `${lang}_${location}`;
18+
}
19+
20+
return Hook.run('set-default-locale', this);
21+
};
22+
23+
getStrings() {
24+
// TODO: ssr version
25+
const defaultStrings = { strings: JSON.stringify({}) };
26+
27+
try {
28+
if (typeof window !== 'undefined') {
29+
const context = require.context(
30+
'babel-loader!@atomic-reactor/webpack-po-loader!reactium-translations',
31+
true,
32+
/.pot?$/,
33+
);
34+
35+
if (
36+
context
37+
.keys()
38+
.find(
39+
translation =>
40+
translation === `./${this.locale}.po`,
41+
)
42+
) {
43+
return context(`./${this.locale}.po`);
44+
}
45+
46+
return context('./template.pot');
47+
} else {
48+
return defaultStrings;
49+
}
50+
} catch (error) {
51+
return defaultStrings;
52+
}
53+
}
54+
55+
getJed() {
56+
return new Jed(JSON.parse(this.getStrings().strings));
57+
}
58+
}
59+
60+
export default new i18n();

.core/sdk/index.js

+3-1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import Component from './component';
33
import Enums from './enums';
44
import Handle from './handle';
55
import Hook from './hook';
6+
import i18n from './i18n';
67
import Middleware from './middleware';
78
import Parse from 'appdir/api';
89
import Plugin from './plugin';
@@ -14,7 +15,7 @@ import User from './user';
1415
import Utils from './utils';
1516
import Zone from './zone';
1617

17-
export * from './react/hooks';
18+
export * from './named';
1819

1920
export default {
2021
...Parse,
@@ -23,6 +24,7 @@ export default {
2324
Enums,
2425
Handle,
2526
Hook,
27+
i18n,
2628
Middleware,
2729
Plugin,
2830
Reducer,
File renamed without changes.
File renamed without changes.

.core/sdk/named/i18n.js

+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import i18n from '../i18n';
2+
3+
/**
4+
* @api {Function} __(text) __()
5+
* @apiDescription Wrap this around string literals to make them translateable with gettext Poedit utility.
6+
Run `arcli i18n` to extract strings to `src/reactium-translations/template.pot` by default.
7+
* @apiName __
8+
* @apiParam {StringLiteral} text the text to be translated. Important: this should not be a variable. It must be a string literal, or
9+
`arcli i18n` command will not be able to locate the string. This string may not be an ES6 template literal.
10+
* @apiGroup i18n
11+
*/
12+
export const __ = (...params) => i18n.getJed().gettext(...params);
13+
14+
/**
15+
* @api {Function} _n(singular,plural,count) _n()
16+
* @apiDescription Wrap this around string literals to make them translateable with gettext Poedit utility.
17+
Run `arcli i18n` to extract strings to `src/reactium-translations/template.pot` by default.
18+
* @apiName _n
19+
* @apiParam {StringLiteral} singular the singular form text to be translated. Important: this should not be a variable. It must be a string literal, or
20+
`arcli i18n` command will not be able to locate the string. This string may not be an ES6 template literal.
21+
* @apiParam {StringLiteral} plural the plural form text to be translated. Important: this should not be a variable. It must be a string literal, or
22+
`arcli i18n` command will not be able to locate the string. This string may not be an ES6 template literal.
23+
* @apiParam {Number} count the number related to singular or plural string
24+
* @apiGroup i18n
25+
*/
26+
export const _n = (...params) => i18n.getJed().ngettext(...params);

.core/sdk/react/hooks.js renamed to .core/sdk/named/index.js

+1
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,4 @@ export * from './redux';
22
export * from './component';
33
export * from './handle';
44
export * from './window';
5+
export * from './i18n';
File renamed without changes.
File renamed without changes.

0 commit comments

Comments
 (0)