i18next / i18next-http-backend

i18next-http-backend is a backend layer for i18next using in Node.js, in the browser and for Deno.
MIT License
452 stars 70 forks source link

Failed request to load remote resources aborts future reloads #147

Closed itsramiel closed 3 months ago

itsramiel commented 3 months ago

πŸ› Bug Report

I am loading my translation resources using a backend. The issue is that if a request fails for some reason then subsequent requests are not attempted even if they were to succeed.

Use case in real case scenario: I have a react native app that loads resources from the backend. It is possible that user opens the app and translations are loaded while he is offline, which leads to a failed request. I reload resourced using i18next.reloadResources() at certain points to make sure translations are up to date, however reloading resources no longer makes a request, which is the bug.

There is a similar issue #142 that was opened but it did not really fix this behavior

To Reproduce

const i18next = require("i18next");
const HttpBackend = require("i18next-http-backend");

let loadPath = "https://your-backend/{{ns}}/{{lng}}.json"; // incorrect backend url to fail requests

i18next
  .use(HttpBackend)
  .init({
    backend: {
      loadPath: loadPath,
      parse(data, languages, namespaces) {
        console.log("languages", languages);
        console.log("namespaces", namespaces);
        return JSON.parse(data);
      },
    },
    lng: "en",
    fallbackLng: "en",
    defaultNS: "translation",
    ns: ["translation"],
    debug: true,
  })
  .then(() => {
    console.log(i18next.t("key"));

    loadPath = "https://your-backend/{{ns}}/{{lng}}.json"; // corrected backend url

    setTimeout(() => {
      console.log("will reload");
      i18next.reloadResources().then(() => {
        console.log("reloaded");
      });
    }, 1000);
  });

If you run this, you will see in debug logs that there was an error because of the wrong url but later on the url was corrected but no request is attempted. Of course you need to pass in actual urls, these are placeholders

Expected behavior

I expect that every time reloadResources is called then a request is made to the backend, and not that subsequent failed requests not go through

Your Environment

adrai commented 3 months ago

Try with i18next-http-backend v2.6.0 and i18next v23.14.0

itsramiel commented 3 months ago

@adrai Thanks for the fix, will give it a try tomorrow and let you know πŸ™

itsramiel commented 3 months ago

Hey @adrai, the new versions did not fix the problem unfortunately. There are is no request made for the reloadResources

adrai commented 3 months ago

I just checked this twice and it seemed to work. Can you provide a little example repository that reproduces it?

itsramiel commented 3 months ago

Hi @adrai here is a sample repo

Try running the app, and you will that the first time it will fail because the url is wrong and then the url is fixed but no request is made and there are logs about a failed/success request. And the key is still untranslated.

If you fix the url from the beginning, both requests are logged and translations are there

adrai commented 3 months ago

You can't just change the loadPath like that... it will not use the new url... you need to do something like this:

i18next.services.backendConnector.backend.options.loadPath = 'https://jsonplaceholder.typicode.com/posts/1'

itsramiel commented 3 months ago

You can't just change the loadPath like that... it will not use the new url... you need to do something like this:

i18next.services.backendConnector.backend.options.loadPath = 'https://jsonplaceholder.typicode.com/posts/1'

Hi @adrai,

Didnt know that is not the correct way. I did that in the sample when I created the issue and didnt receive this feedback.

Anyway I updated repo and you can pull the changes and test that it still doesnt work. Note that this is not what I do in production. This is simply a reproducible example to simulate the issue I am seeing in my react native app.

adrai commented 3 months ago

2 points: 1) there is another option that needs to be set.. 2) the response status code of: https://jsonplaceholder.typicode.com/posts/1 is 404, and this is not retriable... => https://github.com/i18next/i18next-http-backend/blob/master/lib/index.js#L76 so changed your example with a non existing domain...

const i18next = require("i18next");
const HttpBackend = require("i18next-http-backend");

let loadPath = "https://WRONGjsonplaceholder.typicode.com/posts/1";

i18next
  .use(HttpBackend)
  .init({
    backend: {
      loadPath: loadPath,
      customHeaders: {
        "Cache-Control": "no-cache",
        Pragma: "no-cache",
        Expires: "0",
      },
      parse(data, languages, namespaces) {
        console.log("languages", languages);
        console.log("namespaces", namespaces);
        return JSON.parse(data);
      },
    },
    lng: "en",
    fallbackLng: "en",
    defaultNS: "translation",
    ns: ["translation"],
    debug: true,
  })
  .then(() => {
    console.log(i18next.t("userId"));
    console.log(i18next.t("title"));

    // HACK: these 2 options needs to be overwritten, if the i18next instance is init() is not called again
    i18next.options.backend.loadPath = "https://jsonplaceholder.typicode.com/posts/1";
    i18next.services.backendConnector.backend.options.loadPath = i18next.options.backend.loadPath;

    setTimeout(() => {
      console.log("will reload");
      i18next.reloadResources().then(() => {
        console.log(i18next.t("userId"));
        console.log(i18next.t("title"));
        console.log("reloaded");
      });
    }, 1000);
  });
itsramiel commented 3 months ago

Hey @adrai

Alright I guess my reproducible isnt 100% accurate because it returns 404 while my actual error is when there is no internet

My guess is that:

In here https://github.com/i18next/i18next-http-backend/blob/541b25795571d5fc4b9e87c3b062efc7154a5624/lib/index.js#L77

If there was no response, you check for a specific error message, which I believe is the bug? In the browser, you get Failed to fetch error while in react native it is Network request failed which probably is an implementation detail of the js engine. Browser likely v8 while react native uses Hermes


To further check it yourself:

I created a repo that is exactly the case I encounter. The repo can be found here

If you dont know how to run an expo react native app then expand this 1. clone repo, install deps with `yarn install`, and run `yarn start`. Now you should see a qr code in your terminal 2. Install the `expo` app on your ios/android device from the app store/play store 3. Scan the qr code shown in the terminal with your device and just like that the app is running

Here is the normal case when user has internet:

console logs

 WARN  i18next: hasLoadedNamespace: i18next was not initialized ["en"]
    at App (http://192.168.0.75:8081/node_modules/expo/AppEntry.bundle//&platform=ios&dev=true&hot=false&transform.engine=hermes&transform.bytecode=true&transform.routerRoot=app:130363:60)
    at withDevTools(App) (http://192.168.0.75:8081/node_modules/expo/AppEntry.bundle//&platform=ios&dev=true&hot=false&transform.engine=hermes&transform.bytecode=true&transform.routerRoot=app:129917:27)
    at RCTView
    at View (http://192.168.0.75:8081/node_modules/expo/AppEntry.bundle//&platform=ios&dev=true&hot=false&transform.engine=hermes&transform.bytecode=true&transform.routerRoot=app:74557:43)
    at RCTView
    at View (http://192.168.0.75:8081/node_modules/expo/AppEntry.bundle//&platform=ios&dev=true&hot=false&transform.engine=hermes&transform.bytecode=true&transform.routerRoot=app:74557:43)
    at AppContainer (http://192.168.0.75:8081/node_modules/expo/AppEntry.bundle//&platform=ios&dev=true&hot=false&transform.engine=hermes&transform.bytecode=true&transform.routerRoot=app:74400:25)
    at main(RootComponent) (http://192.168.0.75:8081/node_modules/expo/AppEntry.bundle//&platform=ios&dev=true&hot=false&transform.engine=hermes&transform.bytecode=true&transform.routerRoot=app:117830:28)
 WARN  i18next::translator: key "first_name" for languages "en" won't get resolved as namespace "translation" was not yet loaded This means something IS WRONG in your setup. You access the t function before i18next.init / i18next.loadNamespace / i18next.changeLanguage was done. Wait for the callback or Promise to resolve before accessing it!!!
    at App (http://192.168.0.75:8081/node_modules/expo/AppEntry.bundle//&platform=ios&dev=true&hot=false&transform.engine=hermes&transform.bytecode=true&transform.routerRoot=app:130363:60)
    at withDevTools(App) (http://192.168.0.75:8081/node_modules/expo/AppEntry.bundle//&platform=ios&dev=true&hot=false&transform.engine=hermes&transform.bytecode=true&transform.routerRoot=app:129917:27)
    at RCTView
    at View (http://192.168.0.75:8081/node_modules/expo/AppEntry.bundle//&platform=ios&dev=true&hot=false&transform.engine=hermes&transform.bytecode=true&transform.routerRoot=app:74557:43)
    at RCTView
    at View (http://192.168.0.75:8081/node_modules/expo/AppEntry.bundle//&platform=ios&dev=true&hot=false&transform.engine=hermes&transform.bytecode=true&transform.routerRoot=app:74557:43)
    at AppContainer (http://192.168.0.75:8081/node_modules/expo/AppEntry.bundle//&platform=ios&dev=true&hot=false&transform.engine=hermes&transform.bytecode=true&transform.routerRoot=app:74400:25)
    at main(RootComponent) (http://192.168.0.75:8081/node_modules/expo/AppEntry.bundle//&platform=ios&dev=true&hot=false&transform.engine=hermes&transform.bytecode=true&transform.routerRoot=app:117830:28)
 LOG  i18next::translator: missingKey en translation first_name first_name
 LOG  called parse
 LOG  i18next::backendConnector: loaded namespace translation for language en {"address": {"city": "Belleburgh", "coordinates": {"lat": -77.03848548019587, "lng": -158.40973938049999}, "country": "United States", "state": "Connecticut", "street_address": "4609 Eddy Prairie", "street_name": "Marvin Plains", "zip_code": "26384"}, "avatar": "https://robohash.org/etmaioresquo.png?size=300x300&set=set1", "credit_card": {"cc_number": "4300-1044-3323-9307"}, "date_of_birth": "1974-07-15", "email": "buster.harber@email.com", "employment": {"key_skill": "Leadership", "title": "Product Mining Planner"}, "first_name": "Buster", "gender": "Polygender", "id": 3193, "last_name": "Harber", "password": "x2ulB0gRDE", "phone_number": "+595 116-438-7438 x726", "social_insurance_number": "950840389", "subscription": {"payment_method": "Money transfer", "plan": "Platinum", "status": "Pending", "term": "Monthly"}, "uid": "2eb0bdb2-b602-4ee3-b401-ce1fc66e1e23", "username": "buster.harber"}
 LOG  i18next: languageChanged en
 LOG  i18next: initialized {"appendNamespaceToCIMode": false, "appendNamespaceToMissingKey": false, "backend": {"loadPath": "https://random-data-api.com/api/v2/users", "parse": [Function parse]}, "compatibilityJSON": "v3", "contextSeparator": "_", "debug": true, "defaultNS": "translation", "fallbackLng": ["en"], "fallbackNS": false, "ignoreJSONStructure": true, "initImmediate": true, "interpolation": {"escapeValue": true, "format": [Function bound format], "formatSeparator": ",", "maxReplaces": 1000, "nestingOptionsSeparator": ",", "nestingPrefix": "$t(", "nestingSuffix": ")", "prefix": "{{", "skipOnVariables": true, "suffix": "}}", "unescapePrefix": "-"}, "joinArrays": false, "keySeparator": ".", "lng": "en", "load": "all", "missingInterpolationHandler": false, "missingKeyHandler": false, "nonExplicitSupportedLngs": false, "ns": ["translation"], "nsSeparator": ":", "overloadTranslationOptionHandler": [Function overloadTranslationOptionHandler], "parseMissingKeyHandler": false, "partialBundledLanguages": false, "pluralSeparator": "_", "postProcess": false, "postProcessPassResolved": false, "preload": false, "react": {"useSuspense": false}, "returnEmptyString": true, "returnNull": false, "returnObjects": false, "returnedObjectHandler": false, "saveMissing": false, "saveMissingPlurals": true, "saveMissingTo": "fallback", "simplifyPluralSuffix": true, "supportedLngs": false, "updateMissing": false}
 LOG  called parse
 LOG  i18next::backendConnector: loaded namespace translation for language en {"address": {"city": "North Rory", "coordinates": {"lat": 30.893290764917097, "lng": 38.945948564904484}, "country": "United States", "state": "Oklahoma", "street_address": "7587 Gulgowski Underpass", "street_name": "Dallas Locks", "zip_code": "46148-7810"}, "avatar": "https://robohash.org/nihilsuntcum.png?size=300x300&set=set1", "credit_card": {"cc_number": "5121-5155-9625-1884"}, "date_of_birth": "1965-03-24", "email": "reinaldo.mohr@email.com", "employment": {"key_skill": "Fast learner", "title": "Senior Strategist"}, "first_name": "Reinaldo", "gender": "Polygender", "id": 2562, "last_name": "Mohr", "password": "kaKAmOeVJE", "phone_number": "+36 929-081-8861 x61975", "social_insurance_number": "732030481", "subscription": {"payment_method": "Cash", "plan": "Diamond", "status": "Pending", "term": "Payment in advance"}, "uid": "b2cb65c7-e066-4489-a5bf-39391123a9c5", "username": "reinaldo.mohr"}
 LOG  reloaded
 LOG  i18next: languageChanged en
 LOG  called parse
 LOG  i18next::backendConnector: loaded namespace translation for language en {"address": {"city": "Port Danfort", "coordinates": {"lat": -50.96101765653314, "lng": 50.5101549628622}, "country": "United States", "state": "Montana", "street_address": "3588 Tatyana Key", "street_name": "Moore Flat", "zip_code": "23908-6544"}, "avatar": "https://robohash.org/quianumquamexercitationem.png?size=300x300&set=set1", "credit_card": {"cc_number": "5216-2444-9385-5418"}, "date_of_birth": "2004-08-24", "email": "davis.dare@email.com", "employment": {"key_skill": "Teamwork", "title": "Consulting Designer"}, "first_name": "Davis", "gender": "Bigender", "id": 9707, "last_name": "Dare", "password": "bcAxsHUDnY", "phone_number": "+1-671 681.462.6180 x4179", "social_insurance_number": "355898404", "subscription": {"payment_method": "Cheque", "plan": "Basic", "status": "Active", "term": "Monthly"}, "uid": "da769fb3-9b44-4bbe-b6f6-48f96e9ea117", "username": "davis.dare"}
 LOG  reloaded
 LOG  i18next: languageChanged en

Now here is the case where I start offline and then go online:

console logs:

 WARN  i18next: hasLoadedNamespace: i18next was not initialized ["en"]
    at App (http://127.0.0.1:8081/node_modules/expo/AppEntry.bundle//&platform=ios&dev=true&hot=false&transform.engine=hermes&transform.bytecode=true&transform.routerRoot=app:130363:60)
    at withDevTools(App) (http://127.0.0.1:8081/node_modules/expo/AppEntry.bundle//&platform=ios&dev=true&hot=false&transform.engine=hermes&transform.bytecode=true&transform.routerRoot=app:129917:27)
    at RCTView
    at View (http://127.0.0.1:8081/node_modules/expo/AppEntry.bundle//&platform=ios&dev=true&hot=false&transform.engine=hermes&transform.bytecode=true&transform.routerRoot=app:74557:43)
    at RCTView
    at View (http://127.0.0.1:8081/node_modules/expo/AppEntry.bundle//&platform=ios&dev=true&hot=false&transform.engine=hermes&transform.bytecode=true&transform.routerRoot=app:74557:43)
    at AppContainer (http://127.0.0.1:8081/node_modules/expo/AppEntry.bundle//&platform=ios&dev=true&hot=false&transform.engine=hermes&transform.bytecode=true&transform.routerRoot=app:74400:25)
    at main(RootComponent) (http://127.0.0.1:8081/node_modules/expo/AppEntry.bundle//&platform=ios&dev=true&hot=false&transform.engine=hermes&transform.bytecode=true&transform.routerRoot=app:117830:28)
 WARN  i18next::translator: key "first_name" for languages "en" won't get resolved as namespace "translation" was not yet loaded This means something IS WRONG in your setup. You access the t function before i18next.init / i18next.loadNamespace / i18next.changeLanguage was done. Wait for the callback or Promise to resolve before accessing it!!!
    at App (http://127.0.0.1:8081/node_modules/expo/AppEntry.bundle//&platform=ios&dev=true&hot=false&transform.engine=hermes&transform.bytecode=true&transform.routerRoot=app:130363:60)
    at withDevTools(App) (http://127.0.0.1:8081/node_modules/expo/AppEntry.bundle//&platform=ios&dev=true&hot=false&transform.engine=hermes&transform.bytecode=true&transform.routerRoot=app:129917:27)
    at RCTView
    at View (http://127.0.0.1:8081/node_modules/expo/AppEntry.bundle//&platform=ios&dev=true&hot=false&transform.engine=hermes&transform.bytecode=true&transform.routerRoot=app:74557:43)
    at RCTView
    at View (http://127.0.0.1:8081/node_modules/expo/AppEntry.bundle//&platform=ios&dev=true&hot=false&transform.engine=hermes&transform.bytecode=true&transform.routerRoot=app:74557:43)
    at AppContainer (http://127.0.0.1:8081/node_modules/expo/AppEntry.bundle//&platform=ios&dev=true&hot=false&transform.engine=hermes&transform.bytecode=true&transform.routerRoot=app:74400:25)
    at main(RootComponent) (http://127.0.0.1:8081/node_modules/expo/AppEntry.bundle//&platform=ios&dev=true&hot=false&transform.engine=hermes&transform.bytecode=true&transform.routerRoot=app:117830:28)
 LOG  i18next::translator: missingKey en translation first_name first_name
 WARN  i18next::backendConnector: loading namespace translation for language en failed [TypeError: Network request failed]
 LOG  i18next::translator: missingKey en translation first_name first_name
 LOG  i18next: languageChanged en
 LOG  i18next: initialized {"appendNamespaceToCIMode": false, "appendNamespaceToMissingKey": false, "backend": {"loadPath": "https://random-data-api.com/api/v2/users", "parse": [Function parse]}, "compatibilityJSON": "v3", "contextSeparator": "_", "debug": true, "defaultNS": "translation", "fallbackLng": ["en"], "fallbackNS": false, "ignoreJSONStructure": true, "initImmediate": true, "interpolation": {"escapeValue": true, "format": [Function bound format], "formatSeparator": ",", "maxReplaces": 1000, "nestingOptionsSeparator": ",", "nestingPrefix": "$t(", "nestingSuffix": ")", "prefix": "{{", "skipOnVariables": true, "suffix": "}}", "unescapePrefix": "-"}, "joinArrays": false, "keySeparator": ".", "lng": "en", "load": "all", "missingInterpolationHandler": false, "missingKeyHandler": false, "nonExplicitSupportedLngs": false, "ns": ["translation"], "nsSeparator": ":", "overloadTranslationOptionHandler": [Function overloadTranslationOptionHandler], "parseMissingKeyHandler": false, "partialBundledLanguages": false, "pluralSeparator": "_", "postProcess": false, "postProcessPassResolved": false, "preload": false, "react": {"useSuspense": false}, "returnEmptyString": true, "returnNull": false, "returnObjects": false, "returnedObjectHandler": false, "saveMissing": false, "saveMissingPlurals": true, "saveMissingTo": "fallback", "simplifyPluralSuffix": true, "supportedLngs": false, "updateMissing": false}
 LOG  i18next::translator: missingKey en translation first_name first_name
 LOG  i18next::translator: missingKey en translation first_name first_name
 LOG  reloaded
 LOG  i18next::translator: missingKey en translation first_name first_name
 LOG  i18next: languageChanged en
 LOG  reloaded
 LOG  i18next::translator: missingKey en translation first_name first_name
 LOG  i18next: languageChanged en
 LOG  reloaded
 LOG  i18next::translator: missingKey en translation first_name first_name
 LOG  i18next: languageChanged en
 LOG  reloaded
 LOG  i18next::translator: missingKey en translation first_name first_name
 LOG  i18next: languageChanged en
adrai commented 3 months ago

did not try, but can you check if v2.6.1 works?

itsramiel commented 3 months ago

Yes, it does. Thank you!

Closing