wiziple / gatsby-plugin-intl

Gatsby plugin that turns your website into an internationalization-framework out of the box.
http://gatsby-starter-default-intl.netlify.com
325 stars 178 forks source link

md/mdx multilingual blog support? #17

Open rchrdnsh opened 5 years ago

rchrdnsh commented 5 years ago

Got the plugin working on my site for the most part XD

The last issue I'm having is figuring out how to use the plugin for the blog/news section of the site, in which the posts are written in MDX and are multilingual as well.

Any thoughts on how to do this with the plugin?

Thank you @wiziple XD

darekaze commented 5 years ago

I've found out a blog from Hiddentao that setup multilingual blog using this plugin with markdown.

It's ok with .md but I want to try with mdx files using gatsby-mdx. Taking the above blog and this doc as references, It seems doesn't work out with <MDXRenderer> (haven't try it with <MDXProvider> but that way is kinda hacky) and the process to setup this is cumbersome.

I ended up using using-i18n in the official example without any i18n library and works well with mdx file (in my case).

I still like the approach that react-intl provides. Hope this plugin can further support creating pages with md/mdx.

rchrdnsh commented 5 years ago

oh nice, i'll check it out :-)

angrypie commented 5 years ago

@rchrdnsh You can use react-intl with MDX in the same way as with JSX.

If you want to generate pages programmatically, and keep separate files for different languages, you should not rely on this plugin i think. It's different task.

@darekaze If I understand correctly all page translations are sent to the client. Not perfect solution for me. I personally did this by passing correct data into the context of the page that was already generated by gatsby-plugin-intl. Also found out some bug (#43) on the way :)

sn3h commented 5 years ago

If I understand you correctly, you need to transform parts of any data format, which go something like this:

into 2 pages. /en/blogPost and /es/blogPost, each using adequate subtree of data.. if you tell Gatsby to create pages for each language, this plugin creates another language mutations for each of them, so you would have to intercept and delete them in the gatsby-node. Something like onCreatePage ... if(page.url.contains('/en/de/')) deletePage(page)

I just thought that up, want to try it in few days, but might not work. I am still beginner at this :)

sn3h commented 5 years ago

thinking about this more.. Let's say I have blog entry `

then en.json and de.json and some blog.js template (using this plugin with [en, de] as languages)... If blog.js has query depending on language variable data (filter: localization: { eq: $intl.language} }), would this plugin generate /en/blog/test and /de/blog/test correctly? Because we know the current language.. kind of brainstorming, but I hope something like this works. If I find some solution for this, I will let you know ;)

angrypie commented 5 years ago

@sn3h Hey. Look at issue #47, it's already solved :)

sn3h commented 5 years ago

@angrypie thank you, but will it work with changeLocale call?

angrypie commented 5 years ago

@sn3h No, it will not work with translated slug.

rchrdnsh commented 5 years ago

Hi @darekaze,

I checked out the official 'using-i18n' example and mostly everything looks good, except for the language switcher at the top, which takes the user back to the homepage of the other language, rather than staying on the current page, which is a biiiiiiiiiiiiiiiiiiiiig UI/UX no-no. Trying to figure out how to make a switcher that stays on the same page using that example, but I don't really know how to go about it.

If you have any thoughts, I'm all ears.

ruucm commented 4 years ago

@angrypie @sn3h

I got a simple solution for using the changeLocale call.

Just add intl context like the plugin when you create pages from your MDX files

Here are my codes(gatsby-node.js) based on #47

const { createFilePath } = require("gatsby-source-filesystem")
const path = require("path")
const languages = ["en", "ko"] // plugin options

function flattenMessages(nestedMessages, prefix = "") {
  return Object.keys(nestedMessages).reduce((messages, key) => {
    let value = nestedMessages[key]
    let prefixedKey = prefix ? `${prefix}.${key}` : key

    if (typeof value === "string") {
      messages[prefixedKey] = value
    } else {
      Object.assign(messages, flattenMessages(value, prefixedKey))
    }

    return messages
  }, {})
}

const basicPages = new Map()
// Programmatically create the pages for browsing blog posts (Create Page!)
exports.createPages = async ({ graphql, actions, reporter }) => {
  const { createPage } = actions
  const getMessages = (path, language) => {
    try {
      // TODO load yaml here
      const messages = require(`${path}/${language}.json`)

      return flattenMessages(messages)
    } catch (error) {
      if (error.code === "MODULE_NOT_FOUND") {
        process.env.NODE_ENV !== "test" &&
          console.error(
            `[gatsby-plugin-intl] couldn't find file "${path}/${language}.json"`
          )
      }

      throw error
    }
  }

  /**
   * Basic Contents
   */
  const resultsBasic = await graphql(`
    query {
      allMdx(
        sort: { fields: [frontmatter___order], order: ASC }
        filter: { frontmatter: { type: { eq: "basic" } } }
      ) {
        edges {
          node {
            id
            excerpt(pruneLength: 250)
            frontmatter {
              title
              author
              lang
            }
            fields {
              slug
            }
          }
        }
      }
    }
  `)
  // Handle errors
  if (resultsBasic.errors) {
    reporter.panicOnBuild(`🚨 Error while running GraphQL(resultsBasic) query.`)
    return
  }
  console.log("resultsBasic", resultsBasic)
  // you'll call `createPage` for each result
  resultsBasic.data.allMdx.edges.forEach(({ node }, index) => {
    let slug = node.fields.slug
    basicPages.set(`${slug}`, {})
    createPage({
      path: slug,
      // This component will wrap our MDX content
      component: path.resolve(`./src/templates/blog-post-layout.js`),
      context: {
        id: node.id,
        slug: slug,
        prev: index - 1,
        next: index + 1,
        type: "basic",
        // ADD INITL CONTEXT AT HERE
        intl: {
          language: node.frontmatter.lang,
          languages,
          messages: getMessages("./src/intl/", node.frontmatter.lang),
          routed: true,
          originalPath: slug.substr(3), // remove front /en or /ko strings
          redirect: false,
        },
      },
    })
  })
}

// Create Slug!
exports.onCreateNode = async ({
  node,
  actions,
  getNode,
  store,
  cache,
  createNodeId,
}) => {
  const { createNodeField, createNode } = actions
  if (node.internal.type === "Mdx") {
    const value = createFilePath({ node, getNode })
    const newSlug =
      "/" + node.frontmatter.lang + "/clone-apple-music/basic" + value
    createNodeField({
      // Individual MDX node
      node,
      // Name of the field you are adding
      name: "slug",
      // Generated value based on filepath with "blog" prefix
      value: newSlug,
    })
  }
}

// remove duplicated init pages
exports.onCreatePage = async ({ page, actions }) => {
  const { createPage, deletePage } = actions
  // console.log('page.context.type', page.context.type)
  const isBasicPage = page.context.type === "basic"
  const hasInvalidBlogPath = !basicPages.has(page.path)

  // If page is a blog page but has the wrong path
  if (isBasicPage && hasInvalidBlogPath) {
    deletePage(page)
  }
}
rchrdnsh commented 4 years ago

hmmmm...how would you go about changing this example around to support regular mdx files, rather than content coming from a cms @ruucm? It's a bit hard for me to follow :-(

The multi language mdx content is in a folder structure in the project as follows:

src
- content
- - news /* folder of the category type, due to multiple folders of content types */
- - - hipster /* name of the article for the folder */
- - - - index.en.mdx /* or index-en.mdx or anything else that works */
- - - - index.vi.mdx /* or index-vi.mdx or anything else that works */

current node file with some of my guesses as to what should be different:

const { createFilePath } = require("gatsby-source-filesystem")
const path = require("path")
const languages = ["en", "vi"] // plugin options

function flattenMessages(nestedMessages, prefix = "") {
  return Object.keys(nestedMessages).reduce((messages, key) => {
    let value = nestedMessages[key]
    let prefixedKey = prefix ? `${prefix}.${key}` : key

    if (typeof value === "string") {
      messages[prefixedKey] = value
    } else {
      Object.assign(messages, flattenMessages(value, prefixedKey))
    }

    return messages
  }, {})
}

const basicPages = new Map()
// Programmatically create the pages for browsing blog posts (Create Page!)
exports.createPages = async ({ graphql, actions, reporter }) => {
  const { createPage } = actions
  const getMessages = (path, language) => {
    try {
      // TODO load yaml here
      const messages = require(`${path}/${language}.json`)

      return flattenMessages(messages)
    } catch (error) {
      if (error.code === "MODULE_NOT_FOUND") {
        process.env.NODE_ENV !== "test" &&
          console.error(
            `[gatsby-plugin-intl] couldn't find file "${path}/${language}.json"`
          )
      }

      throw error
    }
  }

  /**
   * News Content
   */
  const news = await graphql(`
    query {
      allMdx(
        sort: { order: DESC, fields: [frontmatter___date] }
        filter: { frontmatter: { category: { eq: "news" } } }
      )
      {
        edges {
          node {
            id
            excerpt(pruneLength: 100)
            frontmatter {
              title
              author
              language
            }
            fields {
              slug
            }
          }
        }
      }
    }
  `)
  // Handle errors
  if (news.errors) {
    reporter.panicOnBuild(`🚨 Error while running GraphQL(news) query.`)
    return
  }
  console.log("news", news)
  // you'll call `createPage` for each result
  news.data.allMdx.edges.forEach(({ node }, index) => {
    let slug = node.fields.slug
    basicPages.set(`${slug}`, {})
    createPage({
      path: slug,
      // This component will wrap our MDX content
      component: path.resolve(`./src/templates/article.js`),
      context: {
        id: node.id,
        slug: slug,
        prev: index - 1,
        next: index + 1,
        category: "news",
        // ADD INITL CONTEXT AT HERE
        intl: {
          language: node.frontmatter.language,
          languages,
          messages: getMessages("./src/intl/", node.frontmatter.language),
          routed: true,
          originalPath: slug.substr(3), // remove front /en or /vi strings
          redirect: false,
        },
      },
    })
  })
}

// Create Slug!
exports.onCreateNode = async ({
  node,
  actions,
  getNode,
  store,
  cache,
  createNodeId,
}) => {
  const { createNodeField, createNode } = actions
  if (node.internal.type === "Mdx") {
    const value = createFilePath({ node, getNode })
    const newSlug =
      "/" + node.frontmatter.language + "/" + value
    createNodeField({
      // Individual MDX node
      node,
      // Name of the field you are adding
      name: "slug",
      // Generated value based on filepath with "news" prefix
      value: newSlug,
    })
  }
}

// remove duplicated init pages
exports.onCreatePage = async ({ page, actions }) => {
  const { createPage, deletePage } = actions
  // console.log('page.context.type', page.context.type)
  const isBasicPage = page.context.type === "basic"
  const hasInvalidBlogPath = !basicPages.has(page.path)

  // If page is a blog page but has the wrong path
  if (isBasicPage && hasInvalidBlogPath) {
    deletePage(page)
  }
}

...and from this I'm getting double posts for each language in the blog posts page, and then I'm getting whacky urls like this for each post, which is not working, obviously:

http://localhost:8000/en/en/hipster/index.en/

...soooooooo, yeah...

...no idea what I'm doing...

...just shooting at bugs in the desert at night over here...

samajammin commented 4 years ago

Thanks for the hot tip @ruucm! That did the trick for me 😄

bolonio commented 3 years ago

Is there any real solution to avoid the duplicates??

http://localhost:8000/en/en/SLUG/index.en/
http://localhost:8000/es/es/SLUG/index.es/

Thanks :)