Add localized Routes with Fastify
jaschaio opened this issue ยท 9 comments
๐ Bug Report
It seems that the middleware.addRoute
is not working with fastify.
I get the following errors:
no possibility found to get query
no possibility found to get query
no possibility found to get url
no possibility found to get headers
no possibility found to get cookies
no possibility found to get headers
no possibility found to get headers
no possibility found to get header
no possibility found to set header
no possibility found to get headers
no possibility found to get header
no possibility found to set header
When using fastify.register( middleware.handle( i18next ) )
. I guess middleware.handle
is only for express, as I don't get the errors if I use middleware.plugin
instead โ but the routes are still not working and the documentation doesn't say anything about how to make them work with fastify instead.
Let's say that I am using the middleware.LanguageDetector
and want to have routes in the Following format:
example.org ยป homepage in default language (e.G. 'en')
example.org/de/ ยป homepage in translated language (e.G. 'de')
example.org/es/ ยป homepage in translated language (e.G. 'es')
To Reproduce
import Fastify from 'fastify';
import i18next from 'i18next';
import middleware from 'i18next-http-middleware';
// Initialize i18next
i18next.use( middleware.LanguageDetector ).init();
// Initialize fastify
const fastify = Fastify();
// Add an internationalized route for the homepage
middleware.addRoute(
i18next,
'/:lng/',
[ 'en', 'de', 'es' ],
fastify,
'get',
asnyc ( request, reply ) => {
reply.send( { lng: request.params.lng, language: request.language } );
},
);
// Register i18next middleware plugin with fastify
fastify.register( middleware.plugin, {
i18next,
} );
// Start fastify server โ top level await works since node > v14
await fastify.listen( 3000 );
The language
will be set to the accept-language
header value. But the lng
parameter will be empty, no matter which URL I visit. And I can't change the detected language, even if I visit a URL with a different language prefix.
Your Environment
- runtime version:
15.12.0
- i18next version:
20.1.0
- i18next-http-middleware version:
3.1.0
- os: Linux Buser
This is done on purpose here: https://github.com/i18next/i18next-http-middleware/blob/master/lib/index.js#L204
The question is: Why do you need the lng to be in the request.params? the lng is directly saved in the request object: https://github.com/i18next/i18next-http-middleware/blob/master/test/addRoute.fastify.js#L19
Hey @adrai, thanks for your reply.
I don't need the lng
to be within the request.params
โ I just expected it to be there.
But anyway, even if it's directly on the request
object I would still expect to be able to overwrite the detected language with the /:lng
parameter from the route.
But that doesn't work. And no matter wich route I visit, even the request.lng
stays the same as the detected language.
Adjusted example code:
import Fastify from 'fastify';
import i18next from 'i18next';
import middleware from 'i18next-http-middleware';
// Initialize i18next
i18next.use( middleware.LanguageDetector ).init();
// Initialize fastify
const fastify = Fastify();
// Handler
const handler = async ( request, reply ) => reply.send( { params: request.params, routeLangauge: request.lng, detectedLanguage: request.language } );
// Add i18n routes
middleware.addRoute(
i18next,
'/:lng/',
[ 'en', 'de', 'es' ],
fastify,
'get',
handler,
);
middleware.addRoute(
i18next,
'/:lng/:page',
[ 'en', 'de', 'es' ],
fastify,
'get',
handler,
);
middleware.addRoute(
i18next,
'/:lng/:parent/:page',
[ 'en', 'de', 'es' ],
fastify,
'get',
handler,
);
// Register i18next middleware plugin with fastify
fastify.register( middleware.plugin, {
i18next,
} );
// Start fastify server โ top level await works since node > v14
await fastify.listen( 3000 );
If I visit https://localhost:3000/es/
with a Accept-Language: de,es;q=0.9,en-US;q=0.8,en;q=0.7
header I get:
{"params":{},"detectedLanguage":"de","routeLanguage":"de"}
If I visit https://localhost:3000/es/about
I get:
{"params":{"page":"about"},"detectedLanguage":"de","routeLanguage":"de"}
Ok, I see that if I set the order
to include path
within the LanguageDetector
options it works:
i18next.use( middleware.LanguageDetector ).init( {
detection: {
order: [ 'path', 'header' ],
},
} );
If I then exclude the default language (e.G. en
) from the middleware.addRoute()
call and add a fastify route instead I can get the routes in the format I have described earlier.
Although I am getting some weird results in the route without language prefix then:
// Initialize fastify
const fastify = Fastify();
// Handler
const handler = async ( request, reply ) => reply.send( { params: request.params, routeLangauge: request.lng, detectedLanguage: request.language } );
// Add i18n routes
middleware.addRoute(
i18next,
'/:lng/',
[ 'de', 'es' ],
fastify,
'get',
handler,
);
middleware.addRoute(
i18next,
'/:lng/:page',
[ 'de', 'es' ],
fastify,
'get',
handler,
);
// Add default language routes without language parameter
fastify.get( '/', handler );
fastify.get( '/:page', handler );
// Register i18next middleware plugin with fastify
fastify.register( middleware.plugin, {
i18next,
} );
// Start fastify server โ top level await works since node > v14
await fastify.listen( 3000 );
If I visit https://localhost:3000/es/about/
I get the correct answer:
{"params":{"page":"about"},"routeLangauge":"es","detectedLanguage":"es"}
But if I visit just https://localhost:3000/about/
I get:
{"params":{"page":"about"},"routeLangauge":"about","detectedLanguage":"about"}
about
is obviously not a valid language โ so maybe in the code you shared there should be an extra check if the /:lng
parameter is actually matching one of the specified languages
? https://github.com/i18next/i18next-http-middleware/blob/master/lib/index.js#L203
Would you like to create a PR?
Sure! Taking a look at it the problem seems to be located in https://github.com/i18next/i18next-http-middleware/blob/master/lib/languageLookups/path.js#L11
As the parameter is provided by fastify, it just takes as granted that it's correct โ no matter which value it has (e.G. about
like in the example shared above).
I see two ways to fix this:
1. Either the language detection plugin options takes another argument called languages
thats similar to the third argument the addRoute
method accepts. So for example:
i18next.use( middleware.LanguageDetector ).init( {
detection: {
order: [ 'path', 'header' ],
languages: [ 'es', 'de' ],
},
} );
and then we could check within the line mentioned above if the found
value is part of the options.languages
array.
2. Or we add another option called lookupPathRegex
that makes sure that the found
value matches the regex.
On the other hand I just noticed that you can pass your own detectors, so the problem is solveable as well this way:
var languageDetector = new middleware.LanguageDetector();
languageDetector.addDetector( {
name: 'pathWithDefaultLanguage',
lookup: ( request, reply, options ) => {
let found;
if ( options.lookupPath === undefined || ! req.params )
return;
found = options.getParams(req)[options.lookupPath]
if ( found.match( /\w{2}/ ) === null )
return;
return found;
},
} );
I think what you are looking for is the supportedLngs
option of i18next:
https://www.i18next.com/overview/configuration-options
Yes that worked, thanks!
For anybody having a similar issue and URL structure, I still added this hook to make sure to set the language to en
if the URL doesn't contain a language parameter and redirect users to the correct language version on their first visit:
import fastifyCookie from 'fastify-cookie';
fastify.register( fastifyCookie );
// Set language to 'en' if no language in path and redirect
fastify.addHook( 'preHandler', async ( request, reply ) => {
const { language, url, cookies: { i18next } } = request;
// If we have a path language, do nothing
if ( url.match( /^\/\w{2}(\/[\w]+)*\/$/ ) !== null )
return;
// If we have no language cookie and language is not english, redirect
if ( ! i18next && language !== 'en' )
return reply.redirect( request.url.replace( /^\//, `/${ language }/` ) );
request.i18n.changeLanguage( 'en' );
} );