229 lines
7.8 KiB
JavaScript
229 lines
7.8 KiB
JavaScript
import mongoose from 'mongoose';
|
|
|
|
export default function localizationPlugin(schema, options) {
|
|
if (!options || !options.locales || !Array.isArray(options.locales) || !options.locales.length) {
|
|
throw new mongoose.Error('Required locales array is missing');
|
|
}
|
|
|
|
// plugin options to be set under schema options
|
|
schema.options.localization = {};
|
|
const pluginOptions = schema.options.localization;
|
|
|
|
pluginOptions.locales = options.locales.slice(0);
|
|
|
|
// the first available locale will be used as default if it's not set or unknown value passed
|
|
if (!options.defaultLocale || pluginOptions.locales.indexOf(options.defaultLocale) === -1) {
|
|
pluginOptions.defaultLocale = pluginOptions.locales[0];
|
|
} else {
|
|
pluginOptions.defaultLocale = options.defaultLocale.slice(0);
|
|
}
|
|
|
|
schema.eachPath((path, schemaType) => {
|
|
// TODO: We need a way to set if this should recurse or not. What is happening on global's flexible field is that it tries to save flexibleGlobal.en.en on post.
|
|
let recurse = true;
|
|
if (path.endsWith('global.en')) {
|
|
recurse = false;
|
|
}
|
|
if (schemaType.schema) { // propagate plugin initialization for sub-documents schemas
|
|
schemaType.schema.plugin(localizationPlugin, pluginOptions);
|
|
if (!recurse)
|
|
return;
|
|
}
|
|
|
|
if (!schemaType.options.localized && !(schemaType.schema && schemaType.schema.options.localized)) {
|
|
return;
|
|
}
|
|
|
|
if (!((schemaType instanceof mongoose.Schema.Types.String) || (schemaType instanceof mongoose.Schema.Types.Embedded))) {
|
|
throw new mongoose.Error('localization can only be used on type String or Embedded');
|
|
}
|
|
|
|
let pathArray = path.split('.'),
|
|
key = pathArray.pop(),
|
|
prefix = pathArray.join('.');
|
|
|
|
if (prefix) prefix += '.';
|
|
|
|
// removing real path, it will be changed to virtual later
|
|
schema.remove(path);
|
|
|
|
// schema.remove removes path from paths object only, but doesn't update tree
|
|
// sounds like a bug, removing item from the tree manually
|
|
let tree = pathArray.reduce((mem, part) => {
|
|
return mem[part];
|
|
}, schema.tree);
|
|
delete tree[key];
|
|
|
|
|
|
schema.virtual(path)
|
|
.get(function () {
|
|
|
|
// embedded and sub-documents will use locale methods from the top level document
|
|
let owner = this.ownerDocument ? this.ownerDocument() : this,
|
|
locale = owner.getLocale(),
|
|
localeSubDoc = this.$__getValue(path);
|
|
|
|
if (localeSubDoc === null || localeSubDoc === void 0) {
|
|
return localeSubDoc;
|
|
}
|
|
|
|
let value = localeSubDoc[locale];
|
|
|
|
// If there is no value to return, AKA no translation in locale, handle fallbacks
|
|
if (!value) {
|
|
|
|
// If user specified fallback code as null, send back null
|
|
if (this.fallbackLocale === 'null' || (this.fallbackLocale && !localeSubDoc[this.fallbackLocale])) {
|
|
return null;
|
|
|
|
// If user specified fallback code AND record exists, return that
|
|
} else if (localeSubDoc[this.fallbackLocale]) {
|
|
return localeSubDoc[this.fallbackLocale];
|
|
|
|
// Otherwise, check if there is a default fallback value and if so, send that
|
|
} else if (options.fallback && localeSubDoc[options.defaultLocale]) {
|
|
return localeSubDoc[options.defaultLocale];
|
|
}
|
|
}
|
|
|
|
return value;
|
|
})
|
|
.set(function (value) {
|
|
// multiple locales are set as an object
|
|
if (typeof value === 'object') {
|
|
let locales = this.schema.options.localization.locales;
|
|
locales.forEach(locale => {
|
|
if (!value[locale]) {
|
|
// this.set(`${path}.${locale}`, value);
|
|
return;
|
|
}
|
|
this.set(`${path}.${locale}`, value[locale]);
|
|
}, this);
|
|
}
|
|
|
|
// embedded and sub-documents will use locale methods from the top level document
|
|
let owner = this.ownerDocument ? this.ownerDocument() : this;
|
|
this.set(path + '.' + owner.getLocale(), value);
|
|
});
|
|
|
|
|
|
// localized option is not needed for the current path any more,
|
|
// and is unwanted for all child locale-properties
|
|
// delete schemaType.options.localized; // This was removed to allow viewing inside query parser
|
|
|
|
let localizedObject = {};
|
|
// TODO: setting equal to object is good for hasMany: false, but breaking for hasMany: true;
|
|
// console.log(path, schemaType.options);
|
|
localizedObject[key] = {};
|
|
pluginOptions.locales.forEach(function (locale) {
|
|
let localeOptions = Object.assign({}, schemaType.options);
|
|
if (locale !== options.defaultLocale) {
|
|
delete localeOptions.default;
|
|
delete localeOptions.required;
|
|
}
|
|
|
|
if (schemaType.options.defaultAll) {
|
|
localeOptions.default = schemaType.options.defaultAll;
|
|
}
|
|
|
|
if (schemaType.options.requiredAll) {
|
|
localeOptions.required = schemaType.options.requiredAll;
|
|
}
|
|
|
|
this[locale] = localeOptions;
|
|
}, localizedObject[key]);
|
|
|
|
// localizedObject example:
|
|
// { fieldName: {
|
|
// en: '',
|
|
// de: ''
|
|
// }}
|
|
schema.add(localizedObject, prefix);
|
|
});
|
|
|
|
// document methods to set the locale for each model instance (document)
|
|
schema.method({
|
|
getLocales: function () {
|
|
return this.schema.options.localization.locales;
|
|
},
|
|
getLocale: function () {
|
|
return this.docLocale || this.schema.options.localization.defaultLocale;
|
|
},
|
|
setLocale: function (locale, fallbackLocale) {
|
|
const locales = this.getLocales();
|
|
if (locale && locales.indexOf(locale) !== -1) {
|
|
this.docLocale = locale;
|
|
}
|
|
this.fallbackLocale = fallbackLocale;
|
|
this.schema.eachPath((path, schemaType) => {
|
|
if (schemaType.options.type instanceof Array) {
|
|
this[path] && this[path].forEach(doc => doc.setLocale && doc.setLocale(locale, fallbackLocale));
|
|
}
|
|
|
|
if (schemaType.options.ref && this[path]) {
|
|
this[path] && this[path].setLocale && this[path].setLocale(locale, fallbackLocale);
|
|
}
|
|
})
|
|
},
|
|
unsetLocale: function () {
|
|
delete this.docLocale;
|
|
},
|
|
setFallback: function (fallback = true) {
|
|
pluginOptions.fallback = fallback;
|
|
}
|
|
});
|
|
|
|
// model methods to set the locale for the current schema
|
|
schema.static({
|
|
getLocales: function () {
|
|
return this.schema.options.localization.locales;
|
|
},
|
|
getDefaultLocale: function () {
|
|
return this.schema.options.localization.defaultLocale;
|
|
},
|
|
setDefaultLocale: function (locale) {
|
|
|
|
let updateLocale = function (schema, locale) {
|
|
schema.options.localization.defaultLocale = locale.slice(0);
|
|
|
|
// default locale change for sub-documents schemas
|
|
schema.eachPath((path, schemaType) => {
|
|
if (schemaType.schema) {
|
|
updateLocale(schemaType.schema, locale);
|
|
}
|
|
});
|
|
|
|
};
|
|
|
|
if (locale && this.getLocales().indexOf(locale) !== -1) {
|
|
updateLocale(this.schema, locale);
|
|
}
|
|
}
|
|
});
|
|
|
|
// Mongoose will emit 'init' event once the schema will be attached to the model
|
|
schema.on('init', (model) => {
|
|
// no actions are required in the global method is already defined
|
|
if (model.db.setDefaultLocale) {
|
|
return;
|
|
}
|
|
|
|
// define a global method to change the locale for all models (and their schemas)
|
|
// created for the current mongo connection
|
|
model.db.setDefaultLocale = function (locale) {
|
|
let model, modelName;
|
|
for (modelName in this.models) {
|
|
if (this.models.hasOwnProperty(modelName)) {
|
|
model = this.models[modelName];
|
|
model.setDefaultLocale && model.setDefaultLocale(locale);
|
|
}
|
|
}
|
|
};
|
|
|
|
// create an alias for the global change locale method attached to the default connection
|
|
if (!mongoose.setDefaultLocale) {
|
|
mongoose.setDefaultLocale = mongoose.connection.setDefaultLocale;
|
|
}
|
|
});
|
|
}
|