i18next/i18next-http-middleware

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
adrai commented

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

adrai commented

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;

    },
} );
adrai commented

I think what you are looking for is the supportedLngs option of i18next:
https://www.i18next.com/overview/configuration-options
image

Define it like this:
image

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' );

} );