Gab Social. All are welcome.
This commit is contained in:
40
config/webpack/configuration.js
Normal file
40
config/webpack/configuration.js
Normal file
@@ -0,0 +1,40 @@
|
||||
// Common configuration for webpacker loaded from config/webpacker.yml
|
||||
|
||||
const { resolve } = require('path');
|
||||
const { env } = require('process');
|
||||
const { safeLoad } = require('js-yaml');
|
||||
const { readFileSync } = require('fs');
|
||||
|
||||
const configPath = resolve('config', 'webpacker.yml');
|
||||
const settings = safeLoad(readFileSync(configPath), 'utf8')[env.RAILS_ENV || env.NODE_ENV];
|
||||
|
||||
const themePath = resolve('config', 'themes.yml');
|
||||
const themes = safeLoad(readFileSync(themePath), 'utf8');
|
||||
|
||||
function removeOuterSlashes(string) {
|
||||
return string.replace(/^\/*/, '').replace(/\/*$/, '');
|
||||
}
|
||||
|
||||
function formatPublicPath(host = '', path = '') {
|
||||
let formattedHost = removeOuterSlashes(host);
|
||||
if (formattedHost && !/^http/i.test(formattedHost)) {
|
||||
formattedHost = `//${formattedHost}`;
|
||||
}
|
||||
const formattedPath = removeOuterSlashes(path);
|
||||
return `${formattedHost}/${formattedPath}/`;
|
||||
}
|
||||
|
||||
const output = {
|
||||
path: resolve('public', settings.public_output_path),
|
||||
publicPath: formatPublicPath(env.CDN_HOST, settings.public_output_path),
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
settings,
|
||||
themes,
|
||||
env: {
|
||||
CDN_HOST: env.CDN_HOST,
|
||||
NODE_ENV: env.NODE_ENV,
|
||||
},
|
||||
output,
|
||||
};
|
||||
60
config/webpack/development.js
Normal file
60
config/webpack/development.js
Normal file
@@ -0,0 +1,60 @@
|
||||
// Note: You must restart bin/webpack-dev-server for changes to take effect
|
||||
|
||||
const merge = require('webpack-merge');
|
||||
const sharedConfig = require('./shared');
|
||||
const { settings, output } = require('./configuration');
|
||||
|
||||
const watchOptions = {};
|
||||
|
||||
if (process.env.VAGRANT) {
|
||||
// If we are in Vagrant, we can't rely on inotify to update us with changed
|
||||
// files, so we must poll instead. Here, we poll every second to see if
|
||||
// anything has changed.
|
||||
watchOptions.poll = 1000;
|
||||
}
|
||||
|
||||
module.exports = merge(sharedConfig, {
|
||||
mode: 'development',
|
||||
cache: true,
|
||||
devtool: 'cheap-module-eval-source-map',
|
||||
|
||||
stats: {
|
||||
errorDetails: true,
|
||||
},
|
||||
|
||||
output: {
|
||||
pathinfo: true,
|
||||
},
|
||||
|
||||
devServer: {
|
||||
clientLogLevel: 'none',
|
||||
compress: settings.dev_server.compress,
|
||||
quiet: settings.dev_server.quiet,
|
||||
disableHostCheck: settings.dev_server.disable_host_check,
|
||||
host: settings.dev_server.host,
|
||||
port: settings.dev_server.port,
|
||||
https: settings.dev_server.https,
|
||||
hot: settings.dev_server.hmr,
|
||||
contentBase: output.path,
|
||||
inline: settings.dev_server.inline,
|
||||
useLocalIp: settings.dev_server.use_local_ip,
|
||||
public: settings.dev_server.public,
|
||||
publicPath: output.publicPath,
|
||||
historyApiFallback: {
|
||||
disableDotRule: true,
|
||||
},
|
||||
headers: settings.dev_server.headers,
|
||||
overlay: settings.dev_server.overlay,
|
||||
stats: {
|
||||
entrypoints: false,
|
||||
errorDetails: false,
|
||||
modules: false,
|
||||
moduleTrace: false,
|
||||
},
|
||||
watchOptions: Object.assign(
|
||||
{},
|
||||
settings.dev_server.watch_options,
|
||||
watchOptions
|
||||
),
|
||||
},
|
||||
});
|
||||
52
config/webpack/generateLocalePacks.js
Normal file
52
config/webpack/generateLocalePacks.js
Normal file
@@ -0,0 +1,52 @@
|
||||
// To avoid adding a lot of boilerplate, locale packs are
|
||||
// automatically generated here. These are written into the tmp/
|
||||
// directory and then used to generate locale_en.js, locale_fr.js, etc.
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const rimraf = require('rimraf');
|
||||
const mkdirp = require('mkdirp');
|
||||
|
||||
const localesJsonPath = path.join(__dirname, '../../app/javascript/gabsocial/locales');
|
||||
const locales = fs.readdirSync(localesJsonPath).filter(filename => {
|
||||
return /\.json$/.test(filename) &&
|
||||
!/defaultMessages/.test(filename) &&
|
||||
!/whitelist/.test(filename);
|
||||
}).map(filename => filename.replace(/\.json$/, ''));
|
||||
|
||||
const outPath = path.join(__dirname, '../../tmp/packs');
|
||||
|
||||
rimraf.sync(outPath);
|
||||
mkdirp.sync(outPath);
|
||||
|
||||
const outPaths = [];
|
||||
|
||||
locales.forEach(locale => {
|
||||
const localePath = path.join(outPath, `locale_${locale}.js`);
|
||||
const baseLocale = locale.split('-')[0]; // e.g. 'zh-TW' -> 'zh'
|
||||
const localeDataPath = [
|
||||
// first try react-intl
|
||||
`../../node_modules/react-intl/locale-data/${baseLocale}.js`,
|
||||
// then check locales/locale-data
|
||||
`../../app/javascript/gabsocial/locales/locale-data/${baseLocale}.js`,
|
||||
// fall back to English (this is what react-intl does anyway)
|
||||
'../../node_modules/react-intl/locale-data/en.js',
|
||||
].filter(filename => fs.existsSync(path.join(outPath, filename)))
|
||||
.map(filename => filename.replace(/..\/..\/node_modules\//, ''))[0];
|
||||
|
||||
const localeContent = `//
|
||||
// locale_${locale}.js
|
||||
// automatically generated by generateLocalePacks.js
|
||||
//
|
||||
import messages from '../../app/javascript/gabsocial/locales/${locale}.json';
|
||||
import localeData from ${JSON.stringify(localeDataPath)};
|
||||
import { setLocale } from '../../app/javascript/gabsocial/locales';
|
||||
setLocale({messages, localeData});
|
||||
`;
|
||||
fs.writeFileSync(localePath, localeContent, 'utf8');
|
||||
outPaths.push(localePath);
|
||||
});
|
||||
|
||||
module.exports = outPaths;
|
||||
|
||||
|
||||
107
config/webpack/production.js
Normal file
107
config/webpack/production.js
Normal file
@@ -0,0 +1,107 @@
|
||||
// Note: You must restart bin/webpack-dev-server for changes to take effect
|
||||
|
||||
const path = require('path');
|
||||
const { URL } = require('url');
|
||||
const merge = require('webpack-merge');
|
||||
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer');
|
||||
const OfflinePlugin = require('offline-plugin');
|
||||
const UglifyJsPlugin = require('uglifyjs-webpack-plugin');
|
||||
const CompressionPlugin = require('compression-webpack-plugin');
|
||||
const { output } = require('./configuration');
|
||||
const sharedConfig = require('./shared');
|
||||
|
||||
let attachmentHost;
|
||||
|
||||
if (process.env.S3_ENABLED === 'true') {
|
||||
if (process.env.S3_ALIAS_HOST || process.env.S3_CLOUDFRONT_HOST) {
|
||||
attachmentHost = process.env.S3_ALIAS_HOST || process.env.S3_CLOUDFRONT_HOST;
|
||||
} else {
|
||||
attachmentHost = process.env.S3_HOSTNAME || `s3-${process.env.S3_REGION || 'us-east-1'}.amazonaws.com`;
|
||||
}
|
||||
} else if (process.env.SWIFT_ENABLED === 'true') {
|
||||
const { host } = new URL(process.env.SWIFT_OBJECT_URL);
|
||||
attachmentHost = host;
|
||||
} else {
|
||||
attachmentHost = null;
|
||||
}
|
||||
|
||||
module.exports = merge(sharedConfig, {
|
||||
mode: 'production',
|
||||
devtool: 'source-map',
|
||||
stats: 'normal',
|
||||
bail: true,
|
||||
optimization: {
|
||||
minimize: true,
|
||||
minimizer: [
|
||||
new UglifyJsPlugin({
|
||||
cache: true,
|
||||
parallel: true,
|
||||
sourceMap: true,
|
||||
|
||||
uglifyOptions: {
|
||||
compress: {
|
||||
warnings: false,
|
||||
},
|
||||
|
||||
output: {
|
||||
comments: false,
|
||||
},
|
||||
},
|
||||
}),
|
||||
],
|
||||
},
|
||||
|
||||
plugins: [
|
||||
new CompressionPlugin({
|
||||
filename: '[path].gz[query]',
|
||||
cache: true,
|
||||
test: /\.(js|css|html|json|ico|svg|eot|otf|ttf|map)$/,
|
||||
}),
|
||||
new BundleAnalyzerPlugin({ // generates report.html
|
||||
analyzerMode: 'static',
|
||||
openAnalyzer: false,
|
||||
logLevel: 'silent', // do not bother Webpacker, who runs with --json and parses stdout
|
||||
}),
|
||||
new OfflinePlugin({
|
||||
publicPath: output.publicPath, // sw.js must be served from the root to avoid scope issues
|
||||
caches: {
|
||||
main: [':rest:'],
|
||||
additional: [':externals:'],
|
||||
optional: [
|
||||
'**/locale_*.js', // don't fetch every locale; the user only needs one
|
||||
'**/*_polyfills-*.js', // the user may not need polyfills
|
||||
'**/*.woff2', // the user may have system-fonts enabled
|
||||
// images/audio can be cached on-demand
|
||||
'**/*.png',
|
||||
'**/*.jpg',
|
||||
'**/*.jpeg',
|
||||
'**/*.svg',
|
||||
'**/*.mp3',
|
||||
'**/*.ogg',
|
||||
],
|
||||
},
|
||||
externals: [
|
||||
'/emoji/1f602.svg', // used for emoji picker dropdown
|
||||
'/emoji/sheet_10.png', // used in emoji-mart
|
||||
],
|
||||
excludes: [
|
||||
'**/*.gz',
|
||||
'**/*.map',
|
||||
'stats.json',
|
||||
'report.html',
|
||||
// any browser that supports ServiceWorker will support woff2
|
||||
'**/*.eot',
|
||||
'**/*.ttf',
|
||||
'**/*-webfont-*.svg',
|
||||
'**/*.woff',
|
||||
],
|
||||
ServiceWorker: {
|
||||
entry: `imports-loader?ATTACHMENT_HOST=>${encodeURIComponent(JSON.stringify(attachmentHost))}!${encodeURI(path.join(__dirname, '../../app/javascript/gabsocial/service_worker/entry.js'))}`,
|
||||
cacheName: 'gabsocial',
|
||||
output: '../assets/sw.js',
|
||||
publicPath: '/sw.js',
|
||||
minify: true,
|
||||
},
|
||||
}),
|
||||
],
|
||||
});
|
||||
21
config/webpack/rules/babel.js
Normal file
21
config/webpack/rules/babel.js
Normal file
@@ -0,0 +1,21 @@
|
||||
const { join, resolve } = require('path');
|
||||
const { env, settings } = require('../configuration');
|
||||
|
||||
module.exports = {
|
||||
test: /\.(js|jsx|mjs)$/,
|
||||
include: [
|
||||
settings.source_path,
|
||||
...settings.resolved_paths,
|
||||
].map(p => resolve(p)),
|
||||
exclude: /node_modules/,
|
||||
use: [
|
||||
{
|
||||
loader: 'babel-loader',
|
||||
options: {
|
||||
cacheDirectory: join(settings.cache_path, 'babel-loader'),
|
||||
cacheCompression: env.NODE_ENV === 'production',
|
||||
compact: env.NODE_ENV === 'production',
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
29
config/webpack/rules/css.js
Normal file
29
config/webpack/rules/css.js
Normal file
@@ -0,0 +1,29 @@
|
||||
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
|
||||
|
||||
module.exports = {
|
||||
test: /\.s?css$/i,
|
||||
use: [
|
||||
MiniCssExtractPlugin.loader,
|
||||
{
|
||||
loader: 'css-loader',
|
||||
options: {
|
||||
sourceMap: true,
|
||||
importLoaders: 2,
|
||||
localIdentName: '[name]__[local]___[hash:base64:5]',
|
||||
},
|
||||
},
|
||||
{
|
||||
loader: 'postcss-loader',
|
||||
options: {
|
||||
sourceMap: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
loader: 'sass-loader',
|
||||
options: {
|
||||
implementation: require('sass'),
|
||||
sourceMap: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
20
config/webpack/rules/file.js
Normal file
20
config/webpack/rules/file.js
Normal file
@@ -0,0 +1,20 @@
|
||||
const { join } = require('path');
|
||||
const { settings } = require('../configuration');
|
||||
|
||||
module.exports = {
|
||||
test: new RegExp(`(${settings.static_assets_extensions.join('|')})$`, 'i'),
|
||||
use: [
|
||||
{
|
||||
loader: 'file-loader',
|
||||
options: {
|
||||
name(file) {
|
||||
if (file.includes(settings.source_path)) {
|
||||
return 'media/[path][name]-[hash].[ext]';
|
||||
}
|
||||
return 'media/[folder]/[name]-[hash:8].[ext]';
|
||||
},
|
||||
context: join(settings.source_path),
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
14
config/webpack/rules/index.js
Normal file
14
config/webpack/rules/index.js
Normal file
@@ -0,0 +1,14 @@
|
||||
const babel = require('./babel');
|
||||
const css = require('./css');
|
||||
const file = require('./file');
|
||||
const nodeModules = require('./node_modules');
|
||||
|
||||
// Webpack loaders are processed in reverse order
|
||||
// https://webpack.js.org/concepts/loaders/#loader-features
|
||||
// Lastly, process static files using file loader
|
||||
module.exports = {
|
||||
file,
|
||||
css,
|
||||
nodeModules,
|
||||
babel,
|
||||
};
|
||||
8
config/webpack/rules/mark.js
Normal file
8
config/webpack/rules/mark.js
Normal file
@@ -0,0 +1,8 @@
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
module.exports = {};
|
||||
} else {
|
||||
module.exports = {
|
||||
test: /\.js$/,
|
||||
loader: 'mark-loader',
|
||||
};
|
||||
}
|
||||
23
config/webpack/rules/node_modules.js
Normal file
23
config/webpack/rules/node_modules.js
Normal file
@@ -0,0 +1,23 @@
|
||||
const { join } = require('path');
|
||||
const { settings, env } = require('../configuration');
|
||||
|
||||
module.exports = {
|
||||
test: /\.(js|mjs)$/,
|
||||
include: /node_modules/,
|
||||
exclude: /@babel(?:\/|\\{1,2})runtime/,
|
||||
use: [
|
||||
{
|
||||
loader: 'babel-loader',
|
||||
options: {
|
||||
babelrc: false,
|
||||
plugins: [
|
||||
'transform-react-remove-prop-types',
|
||||
],
|
||||
cacheDirectory: join(settings.cache_path, 'babel-loader-node-modules'),
|
||||
cacheCompression: env.NODE_ENV === 'production',
|
||||
compact: false,
|
||||
sourceMaps: false,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
106
config/webpack/shared.js
Normal file
106
config/webpack/shared.js
Normal file
@@ -0,0 +1,106 @@
|
||||
// Note: You must restart bin/webpack-dev-server for changes to take effect
|
||||
|
||||
const webpack = require('webpack');
|
||||
const { basename, dirname, join, relative, resolve } = require('path');
|
||||
const { sync } = require('glob');
|
||||
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
|
||||
const AssetsManifestPlugin = require('webpack-assets-manifest');
|
||||
const extname = require('path-complete-extname');
|
||||
const { env, settings, themes, output } = require('./configuration');
|
||||
const rules = require('./rules');
|
||||
const localePackPaths = require('./generateLocalePacks');
|
||||
|
||||
const extensionGlob = `**/*{${settings.extensions.join(',')}}*`;
|
||||
const entryPath = join(settings.source_path, settings.source_entry_path);
|
||||
const packPaths = sync(join(entryPath, extensionGlob));
|
||||
|
||||
module.exports = {
|
||||
entry: Object.assign(
|
||||
packPaths.reduce((map, entry) => {
|
||||
const localMap = map;
|
||||
const namespace = relative(join(entryPath), dirname(entry));
|
||||
localMap[join(namespace, basename(entry, extname(entry)))] = resolve(entry);
|
||||
return localMap;
|
||||
}, {}),
|
||||
localePackPaths.reduce((map, entry) => {
|
||||
const localMap = map;
|
||||
localMap[basename(entry, extname(entry, extname(entry)))] = resolve(entry);
|
||||
return localMap;
|
||||
}, {}),
|
||||
Object.keys(themes).reduce((themePaths, name) => {
|
||||
themePaths[name] = resolve(join(settings.source_path, themes[name]));
|
||||
return themePaths;
|
||||
}, {})
|
||||
),
|
||||
|
||||
output: {
|
||||
filename: 'js/[name]-[chunkhash].js',
|
||||
chunkFilename: 'js/[name]-[chunkhash].chunk.js',
|
||||
hotUpdateChunkFilename: 'js/[id]-[hash].hot-update.js',
|
||||
path: output.path,
|
||||
publicPath: output.publicPath,
|
||||
},
|
||||
|
||||
optimization: {
|
||||
runtimeChunk: {
|
||||
name: 'common',
|
||||
},
|
||||
splitChunks: {
|
||||
cacheGroups: {
|
||||
default: false,
|
||||
vendors: false,
|
||||
common: {
|
||||
name: 'common',
|
||||
chunks: 'all',
|
||||
minChunks: 2,
|
||||
minSize: 0,
|
||||
test: /^(?!.*[\\\/]node_modules[\\\/]react-intl[\\\/]).+$/,
|
||||
},
|
||||
},
|
||||
},
|
||||
occurrenceOrder: true,
|
||||
},
|
||||
|
||||
module: {
|
||||
rules: Object.keys(rules).map(key => rules[key]),
|
||||
},
|
||||
|
||||
plugins: [
|
||||
new webpack.EnvironmentPlugin(JSON.parse(JSON.stringify(env))),
|
||||
new webpack.NormalModuleReplacementPlugin(
|
||||
/^history\//, (resource) => {
|
||||
// temporary fix for https://github.com/ReactTraining/react-router/issues/5576
|
||||
// to reduce bundle size
|
||||
resource.request = resource.request.replace(/^history/, 'history/es');
|
||||
}
|
||||
),
|
||||
new MiniCssExtractPlugin({
|
||||
filename: 'css/[name]-[contenthash:8].css',
|
||||
chunkFilename: 'css/[name]-[contenthash:8].chunk.css',
|
||||
}),
|
||||
new AssetsManifestPlugin({
|
||||
integrity: false,
|
||||
entrypoints: true,
|
||||
writeToDisk: true,
|
||||
publicPath: true,
|
||||
}),
|
||||
],
|
||||
|
||||
resolve: {
|
||||
extensions: settings.extensions,
|
||||
modules: [
|
||||
resolve(settings.source_path),
|
||||
'node_modules',
|
||||
],
|
||||
},
|
||||
|
||||
resolveLoader: {
|
||||
modules: ['node_modules'],
|
||||
},
|
||||
|
||||
node: {
|
||||
// Called by http-link-header in an API we never use, increases
|
||||
// bundle size unnecessarily
|
||||
Buffer: false,
|
||||
},
|
||||
};
|
||||
8
config/webpack/test.js
Normal file
8
config/webpack/test.js
Normal file
@@ -0,0 +1,8 @@
|
||||
// Note: You must restart bin/webpack-dev-server for changes to take effect
|
||||
|
||||
const merge = require('webpack-merge');
|
||||
const sharedConfig = require('./shared.js');
|
||||
|
||||
module.exports = merge(sharedConfig, {
|
||||
mode: 'development',
|
||||
});
|
||||
99
config/webpack/translationRunner.js
Normal file
99
config/webpack/translationRunner.js
Normal file
@@ -0,0 +1,99 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { default: manageTranslations } = require('react-intl-translations-manager');
|
||||
|
||||
const RFC5646_REGEXP = /^[a-z]{2,3}(?:-(?:x|[A-Za-z]{2,4}))*$/;
|
||||
|
||||
const rootDirectory = path.resolve(__dirname, '..', '..');
|
||||
const translationsDirectory = path.resolve(rootDirectory, 'app', 'javascript', 'gabsocial', 'locales');
|
||||
const messagesDirectory = path.resolve(rootDirectory, 'build', 'messages');
|
||||
const availableLanguages = fs.readdirSync(translationsDirectory).reduce((languages, filename) => {
|
||||
const basename = path.basename(filename, '.json');
|
||||
if (RFC5646_REGEXP.test(basename)) {
|
||||
languages.push(basename);
|
||||
}
|
||||
return languages;
|
||||
}, []);
|
||||
|
||||
const testRFC5646 = language => {
|
||||
if (!RFC5646_REGEXP.test(language)) {
|
||||
throw new Error('Not RFC5646 name');
|
||||
}
|
||||
};
|
||||
|
||||
const testAvailability = language => {
|
||||
if (!availableLanguages.includes(language)) {
|
||||
throw new Error('Not an available language');
|
||||
}
|
||||
};
|
||||
|
||||
const validateLanguages = (languages, validators) => {
|
||||
const invalidLanguages = languages.reduce((acc, language) => {
|
||||
try {
|
||||
validators.forEach(validator => validator(language));
|
||||
} catch (error) {
|
||||
acc.push({ language, error });
|
||||
}
|
||||
return acc;
|
||||
}, []);
|
||||
|
||||
if (invalidLanguages.length > 0) {
|
||||
console.error(`
|
||||
Error: Specified invalid LANGUAGES:
|
||||
${invalidLanguages.map(({ language, error }) => `* ${language}: ${error.message}`).join('\n')}
|
||||
|
||||
Use yarn "manage:translations -- --help" for usage information
|
||||
`);
|
||||
process.exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
const usage = `Usage: yarn manage:translations [OPTIONS] [LANGUAGES]
|
||||
|
||||
Manage JavaScript translation files in Gab Social. Generates and update translations in translationsDirectory: ${translationsDirectory}
|
||||
|
||||
LANGUAGES
|
||||
The RFC5646 language tag for the language you want to test or fix. If you want to input multiple languages, separate them with space.
|
||||
|
||||
Available languages:
|
||||
${availableLanguages.join(', ')}
|
||||
`;
|
||||
|
||||
const { argv } = require('yargs')
|
||||
.usage(usage)
|
||||
.option('f', {
|
||||
alias: 'force',
|
||||
default: false,
|
||||
describe: 'force using the provided languages. create files if not exists.',
|
||||
type: 'boolean',
|
||||
});
|
||||
|
||||
// check if message directory exists
|
||||
if (!fs.existsSync(messagesDirectory)) {
|
||||
console.error(`
|
||||
Error: messagesDirectory not exists
|
||||
(${messagesDirectory})
|
||||
Try to run "yarn build:development" first`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// determine the languages list
|
||||
const languages = (argv._.length > 0) ? argv._ : availableLanguages;
|
||||
|
||||
// validate languages
|
||||
validateLanguages(languages, [
|
||||
testRFC5646,
|
||||
!argv.force && testAvailability,
|
||||
].filter(Boolean));
|
||||
|
||||
// manage translations
|
||||
manageTranslations({
|
||||
messagesDirectory,
|
||||
translationsDirectory,
|
||||
detectDuplicateIds: false,
|
||||
singleMessagesFile: true,
|
||||
languages,
|
||||
jsonOptions: {
|
||||
trailingNewline: true,
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user