strapi-to-lokalise-plugin
Version:
Preview and sync Lokalise translations from Strapi admin
337 lines (302 loc) âĸ 18.1 kB
JavaScript
;
/**
* Universal version switcher for Strapi v4 + v5 plugins
* Official approach following Strapi documentation:
* - v4: Export routes as plain array [{ method, path, handler, config }]
* - v5: Export routes as FUNCTION: ({ strapi }) => ({ admin: { routes: [...] } })
*
* v5 routes MUST be a function returning { admin: { routes: [...] } } (Official Strapi v5 format).
* v4 routes file exports plain array (required by Strapi v4).
*/
// Load shared components (same for v4 and v5)
const controllers = require('./server/controllers');
const services = require('./server/services');
const contentTypes = require('./server/content-types');
const middlewares = require('./server/middlewares');
// Load route versions
// v4: exports plain array (loaded from v4-compat folder - Strapi v5 won't scan this folder)
// v5: exports plain array from server/routes/index.js (CRITICAL: v5 reads backend API routes from index.js, NOT admin.js)
// CRITICAL: v4 folder renamed to v4-compat to prevent Strapi v5 from auto-scanning it
// Strapi v5 reads backend API routes from server/routes/index.js (plain array)
// admin.js is ONLY for admin panel routing (React pages), NOT backend API endpoints
const routesV4 = require('./server/v4-compat/routes/lokalise-sync');
// Verify v4 is an array
if (!Array.isArray(routesV4)) {
throw new Error(`v4 routes must export an array, got: ${typeof routesV4}`);
}
// Detect Strapi version at module load time
const isV5 = (() => {
try {
const strapiVersion = require('@strapi/strapi/package.json')?.version || null;
return strapiVersion && strapiVersion.startsWith('5');
} catch (_) {
return false;
}
})();
// Return routes based on version
// v4: Plain array (no wrapper)
// v5: Function that returns { type: 'admin', routes: [...] } (REQUIRED by Strapi v5)
module.exports = {
register({ strapi }) {
try {
console.log('\n========================================');
console.log('[lokalise-sync] Plugin register() called');
console.log('========================================\n');
// CRITICAL: Add global request logging for ALL requests to admin API
if (strapi.server && strapi.server.app) {
const app = strapi.server.app;
// Add middleware to log ALL requests before they're routed
app.use(async (ctx, next) => {
// Only log admin API requests
if (ctx.path && ctx.path.startsWith('/admin/api/')) {
console.log('\n========================================');
console.log('[lokalise-sync] đ GLOBAL REQUEST LOGGER');
console.log('========================================');
console.log('[lokalise-sync] Method:', ctx.method);
console.log('[lokalise-sync] Path:', ctx.path);
console.log('[lokalise-sync] URL:', ctx.url);
console.log('[lokalise-sync] Is lokalise-sync route:', ctx.path.includes('lokalise-sync'));
console.log('========================================\n');
}
await next();
});
console.log('[lokalise-sync] â
Global request logger middleware added');
}
console.log('[lokalise-sync] Detected Strapi version:', isV5 ? 'v5' : 'v4');
console.log('[lokalise-sync] Using plugin routes from:', isV5 ? 'server/routes/index.js (v5 - backend API routes)' : 'server/v4-compat/routes/lokalise-sync.js (v4)');
console.log('[lokalise-sync] Routes format:', 'Plain array (both v4 and v5 use same format - per compatibility guide)');
console.log('[lokalise-sync] Routes will be accessible at:', isV5 ? '/admin/api/lokalise-sync/*' : '/lokalise-sync/* (v4 - per compatibility guide)');
// Verify routes export for v5
if (isV5) {
const routesV5 = require('./server/routes/index');
console.log('[lokalise-sync] đ v5 Routes export type:', typeof routesV5);
console.log('[lokalise-sync] đ v5 Routes is array:', Array.isArray(routesV5));
if (Array.isArray(routesV5)) {
console.log('[lokalise-sync] â
v5 Routes is plain array from index.js (CORRECT FORMAT)');
console.log('[lokalise-sync] đ v5 Routes count:', routesV5.length);
const postRoutes = routesV5.filter(r => r.method === 'POST');
console.log('[lokalise-sync] đ v5 POST routes count:', postRoutes.length);
const postSettings = routesV5.find(r => r.method === 'POST' && r.path === '/settings');
console.log('[lokalise-sync] đ v5 POST /settings route:', postSettings ? 'FOUND' : 'NOT FOUND');
} else {
console.error('[lokalise-sync] â v5 Routes is NOT an array! Type:', typeof routesV5);
}
} else {
console.log('[lokalise-sync] Routes count:', routesV4.length);
}
console.log('[lokalise-sync] Using official Strapi admin authentication (admin::isAuthenticatedAdmin)');
console.log('[lokalise-sync] Controllers loaded:', typeof controllers === 'object' ? 'YES' : 'NO');
// CRITICAL: For v5, try to manually verify routes are being exported correctly
if (isV5 && strapi.server && strapi.server.router) {
try {
console.log('[lokalise-sync] đ Attempting to manually check route mounting...');
const exportedRoutes = require('./server/routes/index');
console.log('[lokalise-sync] đ Exported routes type:', typeof exportedRoutes);
console.log('[lokalise-sync] đ Exported routes is array:', Array.isArray(exportedRoutes));
if (Array.isArray(exportedRoutes)) {
console.log('[lokalise-sync] đ First route sample:', JSON.stringify(exportedRoutes[0], null, 2));
}
} catch (e) {
console.error('[lokalise-sync] â Error checking exported routes:', e.message);
}
}
console.log('[lokalise-sync] Plugin registration complete\n');
} catch (error) {
console.error('\n========================================');
console.error('[lokalise-sync] ERROR in register():', error.message);
console.error('[lokalise-sync] Stack:', error.stack);
console.error('========================================\n');
throw error;
}
},
bootstrap({ strapi }) {
console.log('[lokalise-sync] Plugin bootstrap() called');
// Enhanced route verification for debugging
setTimeout(() => {
try {
console.log('\n========================================');
console.log('[lokalise-sync] đ ROUTE VERIFICATION (bootstrap)');
console.log('========================================');
// Check main server router
const router = strapi.server?.router;
if (router) {
const stack = router.stack || router.layers || [];
console.log('[lokalise-sync] Total router stack size:', stack.length);
const lokaliseRoutes = [];
stack.forEach((layer, idx) => {
try {
let path = '';
if (layer.path) path = layer.path;
else if (layer.regexp) path = layer.regexp.source;
else if (layer.route?.path) path = layer.route.path;
if (path && (path.includes('lokalise-sync') || path.includes('lokalise'))) {
const methods = layer.methods || layer.method || Object.keys(layer.route?.methods || {}).filter(m => m !== '_all');
lokaliseRoutes.push({
path,
methods: Array.isArray(methods) ? methods : [methods],
layerIndex: idx,
route: layer.route ? 'exists' : 'none'
});
}
} catch (e) {
// Ignore
}
});
console.log('[lokalise-sync] Found lokalise-sync routes in main router:', lokaliseRoutes.length);
if (lokaliseRoutes.length > 0) {
console.log('[lokalise-sync] Registered routes:');
lokaliseRoutes.forEach((route, idx) => {
console.log(`[lokalise-sync] ${idx + 1}. ${route.methods.join(',')} ${route.path} (layer: ${route.layerIndex})`);
});
// Check specifically for POST routes
const postSettings = lokaliseRoutes.find(r => r.path.includes('settings') && r.methods.includes('POST'));
const postTest = lokaliseRoutes.find(r => r.path.includes('settings/test') && r.methods.includes('POST'));
console.log('[lokalise-sync] POST /settings route registered:', !!postSettings);
console.log('[lokalise-sync] POST /settings/test route registered:', !!postTest);
} else {
if (isV5) {
console.log('[lokalise-sync] â ī¸ No lokalise-sync routes found in main router stack');
console.log('[lokalise-sync] âšī¸ This may be expected - v5 admin routes register differently');
console.log('[lokalise-sync] âšī¸ Checking admin router...');
} else {
console.log('[lokalise-sync] â ī¸ No routes found - check route registration');
}
}
} else {
console.log('[lokalise-sync] â ī¸ Router not available in strapi.server');
}
// CRITICAL: Check admin router for v5 (admin routes are in separate router)
if (isV5) {
try {
console.log('[lokalise-sync] đ CHECKING ADMIN ROUTER (v5 specific)...');
// Try to access admin router
const adminRouter = strapi.server?.router?.routes?.get?.('admin') ||
strapi.server?.app?.router?.routes?.get?.('admin') ||
strapi.admin?.router ||
null;
if (adminRouter) {
console.log('[lokalise-sync] â
Admin router found');
console.log('[lokalise-sync] Admin router type:', typeof adminRouter);
console.log('[lokalise-sync] Admin router keys:', Object.keys(adminRouter));
// Try to find routes
const adminStack = adminRouter.stack || adminRouter.layers || adminRouter.routes || [];
console.log('[lokalise-sync] Admin router stack/layers/routes:', Array.isArray(adminStack) ? adminStack.length : 'not array');
if (Array.isArray(adminStack)) {
const adminLokaliseRoutes = [];
adminStack.forEach((layer, idx) => {
try {
let path = '';
if (layer.path) path = layer.path;
else if (layer.regexp) path = layer.regexp.source;
else if (layer.route?.path) path = layer.route.path;
if (path && (path.includes('lokalise-sync') || path.includes('lokalise'))) {
const methods = layer.methods || layer.method || Object.keys(layer.route?.methods || {}).filter(m => m !== '_all');
adminLokaliseRoutes.push({
path,
methods: Array.isArray(methods) ? methods : [methods],
layerIndex: idx
});
}
} catch (e) {
// Ignore
}
});
console.log('[lokalise-sync] Found lokalise-sync routes in admin router:', adminLokaliseRoutes.length);
if (adminLokaliseRoutes.length > 0) {
adminLokaliseRoutes.forEach((route, idx) => {
console.log(`[lokalise-sync] Admin Route ${idx + 1}. ${route.methods.join(',')} ${route.path}`);
});
const adminPostSettings = adminLokaliseRoutes.find(r => r.path.includes('settings') && r.methods.includes('POST'));
console.log('[lokalise-sync] â
POST /settings in admin router:', !!adminPostSettings);
} else {
console.log('[lokalise-sync] â No lokalise-sync routes found in admin router');
}
}
} else {
console.log('[lokalise-sync] â ī¸ Admin router not found or not accessible');
console.log('[lokalise-sync] Available paths:');
console.log('[lokalise-sync] - strapi.server.router:', !!strapi.server?.router);
console.log('[lokalise-sync] - strapi.server.router.routes:', !!strapi.server?.router?.routes);
console.log('[lokalise-sync] - strapi.server.app:', !!strapi.server?.app);
console.log('[lokalise-sync] - strapi.admin:', !!strapi.admin);
console.log('[lokalise-sync] - strapi.admin.router:', !!strapi.admin?.router);
}
} catch (e) {
console.error('[lokalise-sync] â Error checking admin router:', e.message);
console.error('[lokalise-sync] Stack:', e.stack);
}
}
// Also check if routes are accessible via plugin system
try {
const plugin = strapi.plugin('lokalise-sync');
if (plugin) {
console.log('[lokalise-sync] Plugin instance found:', !!plugin);
console.log('[lokalise-sync] Plugin routes type:', typeof plugin.routes);
// CRITICAL: Check what Strapi actually stored
if (typeof plugin.routes === 'function') {
try {
const pluginRoutes = plugin.routes();
console.log('[lokalise-sync] â
Plugin routes() is function - calling it...');
console.log('[lokalise-sync] Plugin routes() result type:', typeof pluginRoutes);
console.log('[lokalise-sync] Plugin routes() result keys:', pluginRoutes ? Object.keys(pluginRoutes) : []);
console.log('[lokalise-sync] Plugin routes() has admin:', pluginRoutes ? 'admin' in pluginRoutes : false);
if (pluginRoutes?.admin) {
console.log('[lokalise-sync] Plugin routes() admin keys:', Object.keys(pluginRoutes.admin));
console.log('[lokalise-sync] Plugin routes() admin.routes exists:', !!pluginRoutes.admin.routes);
console.log('[lokalise-sync] Plugin routes() admin.routes is array:', Array.isArray(pluginRoutes.admin.routes));
if (pluginRoutes?.admin?.routes) {
console.log('[lokalise-sync] Plugin routes() admin.routes count:', pluginRoutes.admin.routes.length);
const postRoutes = pluginRoutes.admin.routes.filter(r => r.method === 'POST');
console.log('[lokalise-sync] Plugin routes() POST routes count:', postRoutes.length);
// Check for POST /settings specifically
const postSettingsRoute = pluginRoutes.admin.routes.find(r =>
r.method === 'POST' && r.path === '/settings'
);
console.log('[lokalise-sync] Plugin routes() POST /settings route:', postSettingsRoute ? 'FOUND' : 'NOT FOUND');
if (postSettingsRoute) {
console.log('[lokalise-sync] POST /settings handler:', postSettingsRoute.handler);
console.log('[lokalise-sync] POST /settings config:', JSON.stringify(postSettingsRoute.config, null, 2));
}
}
} else {
console.log('[lokalise-sync] â Plugin routes() result does NOT have admin key!');
console.log('[lokalise-sync] Plugin routes() full result:', JSON.stringify(pluginRoutes, null, 2));
}
} catch (e) {
console.error('[lokalise-sync] â Error calling plugin.routes():', e.message);
console.error('[lokalise-sync] Stack:', e.stack);
}
} else {
console.log('[lokalise-sync] â ī¸ Plugin routes is NOT a function, type:', typeof plugin.routes);
console.log('[lokalise-sync] Plugin routes value:', plugin.routes);
}
}
} catch (e) {
console.log('[lokalise-sync] â Could not access plugin instance:', e.message);
console.log('[lokalise-sync] Error stack:', e.stack);
}
console.log('========================================\n');
} catch (err) {
console.error('[lokalise-sync] ERROR checking routes:', err.message);
console.error('[lokalise-sync] Stack:', err.stack);
}
}, 3000); // Increased timeout to 3 seconds
},
// Routes: Export exactly as Strapi expects
// v4: Export routes structure { admin: require('./admin-v4-wrapper.js') } which wraps { type: 'admin', routes: [...] }
// This matches official Strapi v4 plugin structure where server/routes/index.js exports { admin: require('./admin') }
// And admin.js exports { type: 'admin', routes: [...] }
// v5: Plain array from server/routes/index.js (CRITICAL: v5 reads backend API routes from index.js, NOT admin.js)
// Routes: Export exactly as Strapi expects
// v4: Plain array (per compatibility guide - v4 routes MUST be plain array, NO wrapper)
// Per user template: admin routes should be in server/routes/admin.js as plain array
// Strapi v4 will auto-load and make accessible at /admin/api/lokalise-sync/*
// v5: Plain array from server/routes/index.js
routes: isV5
? require('./server/routes/index') // v5: Backend API routes (plain array)
: routesV4, // v4: Plain array (NO wrapper - per compatibility guide)
controllers,
services,
contentTypes,
middlewares,
};