Vulcan/packages/vulcan-lib/lib/server/intl.js
2019-01-30 21:26:43 +01:00

180 lines
5.8 KiB
JavaScript

// see https://github.com/apollographql/graphql-tools/blob/master/docs/source/schema-directives.md#marking-strings-for-internationalization
import { addGraphQLDirective, addGraphQLSchema } from '../modules/graphql';
import { SchemaDirectiveVisitor } from 'graphql-tools';
import { defaultFieldResolver } from 'graphql';
import { Collections } from '../modules/collections';
import { getSetting } from '../modules/settings';
import { debug } from '../modules/debug';
import Vulcan from '../modules/config';
import { isIntlField } from '../modules/intl';
import { Connectors } from './connectors';
import pickBy from 'lodash/pickBy';
/*
Create GraphQL types
*/
const intlValueSchemas = `type IntlValue {
locale: String
value: String
}
input IntlValueInput{
locale: String
value: String
}`;
addGraphQLSchema(intlValueSchemas);
/*
Take an array of translations, a locale, and a default locale, and return a matching string
*/
const getLocaleString = (translations, locale, defaultLocale) => {
const localeObject = translations.find(translation => translation.locale === locale);
const defaultLocaleObject = translations.find(
translation => translation.locale === defaultLocale
);
return (localeObject && localeObject.value) || (defaultLocaleObject && defaultLocaleObject.value);
};
/*
GraphQL @intl directive resolver
*/
class IntlDirective extends SchemaDirectiveVisitor {
visitFieldDefinition(field, details) {
const { resolve = defaultFieldResolver, name } = field;
field.resolve = async function(...args) {
const [doc, graphQLArguments, context] = args;
const fieldValue = await resolve.apply(this, args);
const locale = graphQLArguments.locale || context.locale;
const defaultLocale = getSetting('locale');
const intlField = doc[`${name}_intl`];
// Return string in requested or default language, or else field's original value
return (intlField && getLocaleString(intlField, locale, defaultLocale)) || fieldValue;
};
}
}
addGraphQLDirective({ intl: IntlDirective });
addGraphQLSchema('directive @intl on FIELD_DEFINITION');
/*
Migration function
*/
const migrateIntlFields = async defaultLocale => {
if (!defaultLocale) {
throw new Error(
"Please pass the id of the locale to which to migrate your current content (e.g. migrateIntlFields('en'))"
);
}
Collections.forEach(async collection => {
const schema = collection.simpleSchema()._schema;
const intlFields = pickBy(schema, isIntlField);
const intlFieldsNames = Object.keys(intlFields);
if (intlFieldsNames.length) {
// eslint-disable-next-line no-console
console.log(
`### Found ${intlFieldsNames.length} field to migrate for collection ${
collection.options.collectionName
}: ${intlFieldsNames.join(', ')} ###\n`
);
// const intlFieldsWithLocale = intlFieldsNames.map(f => `${f}_intl`);
// find all documents with one or more unmigrated intl fields
const selector = {
$or: intlFieldsNames.map(f => {
return {
$and: [{ [`${f}`]: { $exists: true } }, { [`${f}_intl`]: { $exists: false } }],
};
}),
};
const documentsToMigrate = await Connectors.find(collection, selector);
if (documentsToMigrate.length) {
console.log(`-> found ${documentsToMigrate.length} documents to migrate \n`); // eslint-disable-line no-console
for (const doc of documentsToMigrate) {
console.log(`// Migrating document ${doc._id}`); // eslint-disable-line no-console
const modifier = { $push: {} };
intlFieldsNames.forEach(f => {
if (doc[f] && !doc[`${f}_intl`]) {
const translationObject = { locale: defaultLocale, value: doc[f] };
console.log(`-> Adding field ${f}_intl: ${JSON.stringify(translationObject)} `); // eslint-disable-line no-console
modifier.$push[`${f}_intl`] = translationObject;
}
});
if (!_.isEmpty(modifier.$push)) {
// update document
// eslint-disable-next-line no-await-in-loop
const n = await Connectors.update(collection, { _id: doc._id }, modifier);
console.log(`-> migrated ${n} documents \n`); // eslint-disable-line no-console
}
console.log('\n'); // eslint-disable-line no-console
}
} else {
console.log('-> found no documents to migrate.'); // eslint-disable-line no-console
}
}
});
};
Vulcan.migrateIntlFields = migrateIntlFields;
/*
Take a header object, and figure out the locale
Also accepts userLocale to indicate the current user's preferred locale
*/
export const getHeaderLocale = (headers, userLocale) => {
let cookieLocale, acceptedLocale, locale, localeMethod;
// get locale from cookies
if (headers['cookie']) {
const cookies = {};
headers['cookie'].split('; ').forEach(c => {
const cookieArray = c.split('=');
cookies[cookieArray[0]] = cookieArray[1];
});
cookieLocale = cookies.locale;
}
// get locale from accepted-language header
if (headers['accept-language']) {
const acceptedLanguages = headers['accept-language'].split(',').map(l => l.split(';')[0]);
acceptedLocale = acceptedLanguages[0]; // for now only use the highest-priority accepted language
}
if (headers.locale) {
locale = headers.locale;
localeMethod = 'header';
} else if (cookieLocale) {
locale = cookieLocale;
localeMethod = 'cookie';
} else if (userLocale) {
locale = userLocale;
localeMethod = 'user';
} else if (acceptedLocale) {
locale = acceptedLocale;
localeMethod = 'browser';
} else {
locale = getSetting('locale', 'en-US');
localeMethod = 'setting';
}
debug(`// locale: ${locale} (via ${localeMethod})`);
return locale;
};