Is there any way to access `__filename` in Next.js?

Viewed 511

I'm working on a custom i18n module and would love to replace this code (this is a an "about-us" page):

  const messages = (await import(`./about-us.${locale}.json`))
    .default as Messages;

By

  const messages = (
    await import(`./${__filename.replace('.tsx', `.${locale}.json`)}`)
  ).default as Messages;

Unfortunately __filename resolves to /index.js (I guess because of Webpack?) - is there any way to achieve what am I trying to do in my example or this would need to be built-in Next.js directly to work?

3 Answers

Refactor this so consumers don't know about the filesystem

Spoiler: I'm not going to tell you how to access __filename with Next.js; I don't know anything about that.

Here's a pattern that is better than what you propose, and which evades the problem entirely.

First, setup: it sounds like you've got a folder filled with these JSON files. I imagine this:

l10n/
    about-us.en-US.json
    about-us.fr-FR.json
    contact-us.en-US.json
    contact-us.fr-FR.json
    ... <package>.<locale>.json

That file organization is nice, but it's a mistake to make every would-be l10n consumer know about it.

  • What if you change the naming scheme later? Are you going to hand-edit every file that imports localized text? Why would you treat Future-You so poorly?
  • If a particular locale file doesn't exist, would you prefer the app crash, or just fall back to some other language?1

It's better to create a function that takes packageName and localeCode as arguments, and returns the desired content. That function then becomes the only part of the app that has to know about filenames, fallback logic, etc.

// l10n/index.js
export default function getLang( packageName, localeCode ) {
    let contentPath = `${packageName}.${localeCode}.json`
    // TODO: fallback logic
    return JSON.parse(FS.readFileSync(contentPath, 'utf8'))
}

It is a complex job to locate and read the desired data while also ensuring that no request ever gets an empty payload and that each text key resolves to a value. Dynamic import + sane filesystem is a good start (:applause:), but that combination is not nearly robust-enough on its own.

At a previous job, we built an entire microservice just to do this one thing. (We also built a separate service for obtaining translations, and a few private npm packages to allow webapps to request and use language packs from our CMS.) You don't have to take it that far, but it hopefully illustrates that the problem space is not tiny.


1 Fallback logic: e.g. en-UK & en-US are usually interchangeable; some clusters of Romance languages might be acceptable in an emergency (Spanish/Portuguese/Brazilian come to mind); also Germanic languages, etc. What works and doesn't depends on the content and context, but no version of fallback will fit into a dynamic import.

You can access __filename in getStaticProps and getServerSideProps if that helps?

I pass __filename to a function that needs it (which has a local API fetch in it), before returning the results to the render.

export async function getStaticProps(context) {
  return {
    props: {
      html: await getData({ page: __filename })
    }, // will be passed to the page component as props
  };
}

After a long search, the solution was to write a useMessages hook that would be "injected" with the correct strings using a custom Babel plugin.

A Webpack loader didn't seem like the right option as the loader only has access to the content of the file it loads. By using Babel, we have a lot more options to inject code into the final compiled version.

Related