gatsby-source-wordpress create pages programmatically for translated pages with wpml

Created on 8 Jan 2019  Âˇ  26Comments  Âˇ  Source: gatsbyjs/gatsby

Summary

Hello,

i am having trouble setting up the multilingual feature for my gatsby app. i have setup all the relevant stuff in my wp backend and translated the relevant pages already. what i was trying to do next was to programmatically create the pages for original language and translated page via the gatsby-node.js like below.

    graphql(
      `
        {
          allWordpressPage {
            edges {
              node {
                id
                slug
                wpml_current_locale
                status
                template
              }
            }
          }
        }
      `
    )
      .then(result => {
        if (result.errors) {
          console.log(result.errors);
          reject(result.errors);
        }
        // Create those pages with the wp_page.jsx template.

        _.each(result.data.allWordpressPage.edges, edge => {
          createPage({
            path: `${edge.node.wpml_current_locale}/${edge.node.slug}/`,
            component: (() => {
              if (edge.node.wordpress_id === 20 || edge.node.wordpress_id === 900) {
                return slash(path.resolve(`./src/pages/about.jsx`));
              }
              return slash(path.resolve(`./src/templates/wp_page.jsx`));
            })(),
            context: {
              id: edge.node.id,
              language: edge.node.wpml_current_locale
            }
          });
        });
      })

the problem i have right now is that if i query all my wordpress pages like that i only recieve the pages in the original language.

my wpml is configured to use a language parameter for the language specific url formats. so i guess the problem is that i only query the default language with my baseUrl. does anyone know how i can query either all pages (original and translated) at once or how could structure this alternatively to work

Most helpful comment

hahahah ^^
pretty sad that they still did not fix that. it´s not like it is a cheap or free plugin lol. yeah switch to polylang imo. i have had no problem with it and already used it in three projects since then. if you have questions about migration feel free to ask.

All 26 comments

my wpml is configured to use a language parameter for the language specific url formats. so i guess the problem is that i only query the default language with my baseUrl. does anyone know how i can query either all pages (original and translated) at once or how could structure this alternatively to work

Not familiar with wpml but quick question—does allWordpressPage include pages in all languages or just the current language?

If the plugin only fetches pages for the current language, then this might require a patch to gatsby-source-wordpress

@sidharthachatterjee it only includes current language but i dont think this i a problem with gatsby-source-wordpress because if i check http://jmb.dgn.mybluehost.me/wp-json/wp/v2/pages there are only the pages in original language as well. to get the english ones i need to prepend ?lang=en to the url.
the way i see it gatsby uses this endpoint to fetch the pages with graphql /wp-json/wp/v2/pages so probably this is a problem of wpml or just the way it works.

but right now i dont see any good way how to fetch those pages in gatsby.

Did you install wpml-rest-api as documented in gatsby-source-wordpress?

Also, It seems that your query is missing something:

        wpml_translations {
          locale
          wordpress_id
          post_title
          href
        }

@cardiv yeah the wmpl-rest-api is installed but its only used to add current locale and the link to the translation to the api entry. and yeah i dont really need that referenced query block. i think basically the problem is that wpml only adds the default language entries to the pages api endpoint and the translated page entries are only available if the lang=en parameter is appended

Your current query fetches only the default locale. If you want to create pages from the translated ones, you need to query these too. The query block I posted does that: It queries the translated page entries you want.

Or what are you trying to do?

@cardiv sorry i dont really know what you mean here. how should this code nested inside the node influence the nodes above being fetched? the problem is that the translated pages are not even showing up in the api response.

I tried that locally and, to be honest, after a closer look at the use case I can see your issue now.

Since I want to do that in the future too, I have worked out a PR that is on the way to handle parameters with custom routes. So you could request /wp/v2/pages/?lang=en and you have the translated pages.

But I have some trouble getting the fetched response into GraphQL. Hopefully, this will get merged soon!

@crstnio Any update on this ?

@isengartz as far as i know this was a bug with wpml and they said to fix it back then. do they still only return the default language pages in the api route for the pages?
I did not follow it because i switched to polylang because of that issue

@arturhenryy Yes its still like this. They provided this code and they claimed that it fixes the issue:

add_action('rest_api_init', function () { if (defined('REST_REQUEST') && REST_REQUEST) { add_action('parse_query', function( $q ) { $q->query_vars['suppress_filters'] = true; }); } });

Which then indeed returns all languages but breaks the ACF-TO-API plugin. I spoke with them at support and they told me to tell the ACF-TO-API owner to fix HIS plugin in order to work with their hook!!!!

I think Im going to refactor to polylang too.

hahahah ^^
pretty sad that they still did not fix that. it´s not like it is a cheap or free plugin lol. yeah switch to polylang imo. i have had no problem with it and already used it in three projects since then. if you have questions about migration feel free to ask.

@arturhenryy if I may ask, how did you solve multilanguage situation for index.js page using Gatsby + Wordpress + Polylang.

I am using createPage in gatsby-node.js for other pages but having difficulties for that particluar index page. Hope you can suggest some solution?

hey @TomePale it should make no difference if it is the index or some other page. all should be created in the createPage. the only difference is that for the index.js you might need to do some special path logic because you want it to be available throug / and not your generic logic of /{$slug}

can you show me your code and explain your problem in a bit more detail or show me the error log?

exports.createPages = async ({ graphql, actions }) => {
  const { createPage } = actions

  const FeatureTemplate = path.resolve('./src/templates/features.js')

  const result = await graphql(`
    query {
      pages: allWordpressPage {
        edges {
          node {
            wordpress_id
            link
            path
            slug
            polylang_current_lang
          }
        }
      }
    }
  `)

  if (result.errors) {
    throw new Error(result.errors)
  }

  const { pages } = result.data

  pages.edges.forEach(page => {
    const { wordpress_id, link, polylang_current_lang } = page.node

    createPage({
      path: link,
      component: (() => {
        if (link === '/' || link === '/en/') {
          return slash(path.resolve(`./src/pages/index.js`))
        }
        return slash(FeatureTemplate)
      })(),
      context: {
        id: wordpress_id,
        locale: polylang_current_lang,
      },
    })
  })
}

So basicly I always get render properly http://localhost:8000/en even the Context object is passed but as soon I go to http://localhost:8000/ it wont work any more becuase I got some graphql queries in index.js page that needs $locale variable (funny thing is that works for EN lang) that is not sent for some reason.

Also link is containing these paths and I am getting them from WordPress:

  • /
  • /en
  • /en/features
  • /prednosti

This is how GraphQL output query looks like:

{
  "data": {
    "pages": {
      "edges": [
        {
          "node": {
            "wordpress_id": 289,
            "link": "/en/features/",
            "path": "/en/features/",
            "slug": "features",
            "polylang_current_lang": "en_GB"
          }
        },
        {
          "node": {
            "wordpress_id": 287,
            "link": "/prednosti/",
            "path": "/prednosti/",
            "slug": "prednosti",
            "polylang_current_lang": "hr"
          }
        },
        {
          "node": {
            "wordpress_id": 282,
            "link": "/",
            "path": "/",
            "slug": "naslovna",
            "polylang_current_lang": "hr"
          }
        },
        {
          "node": {
            "wordpress_id": 280,
            "link": "/en/",
            "path": "/en/",
            "slug": "home",
            "polylang_current_lang": "en_GB"
          }
        }
      ]
    }
  }
}

Thx for helping out !

ok can you post the error you get when going to http://localhost:8000/? i always used lang instead of polylang_current_lang not sure if that makes a difference though. so your code looks alright so far. can you show me the index template query as well?

ah ok i think i found the error. you need to put index.js in the template folder and not within pages.
src/pages/index.js -> src/templates/index.js and then adjust the query accordingly to query inside the index.js by id if you are not doing it already.

always put the page template for programatically generated pages inside the templates folder and never in pages folder. the same would go for blog posts generated inside the gatsby-node.js

Code form index.js

const IndexPage = ({ data, pageContext }) => {
  console.log('pageContext: ', pageContext)
  console.log('data', data)

  return (
    <Layout>
      <SEO title="Home" />
      <Header title={data.wordpressPage.acf.title} subtitle={data.wordpressPage.acf.subtitle} />
      <main className="main-content">
        <OtaChannels />
        <Property />
        <Join />
        <Feature />
        <PromoterScore />
        <FeaturedCustomer />
        <CallToAction />
        <BlogStories />
      </main>
      <Footer />
    </Layout>
  )
}

export default IndexPage

export const query = graphql`
  query($id: Int, $locale: String!) {
    wordpressPage(wordpress_id: { eq: $id }, polylang_current_lang: { eq: $locale }) {
      title
      wordpress_id
      slug
      polylang_current_lang
      acf {
        title
        subtitle
        ota_text
      }
    }
  }
`

Error is TypeError: Cannot read property 'wordpressPage' of undefined but I am aware of that why I am getting it.

However will try this src/pages/index.js -> src/templates/index.js

Will let you know the result...

and you also need to only query by id.

export const query = graphql`
  query($id: Int) {
    wordpressPage(wordpress_id: { eq: $id }) {
      title
      wordpress_id
      slug
      polylang_current_lang
      acf {
        title
        subtitle
        ota_text
      }
    }
  }
`

Thank you so much 😀! You saved the day.

@TomePale haha glad it worked. You are welcome

@arturhenryy Hello! Im stuck to handle translations with wordpress and gatsby... could you please help me? How can i build in gatsby-node the pages for example /de/home for german and /en/home for english? Thanks for the help

@foja123 what translation plugin do you use? can you send the code from your gatsby-node?

@foja123 Im posting my whole gatsby-node for reference for anyone that may find it useful. The part that you want its at the end.
``const locales = require("./src/constants/locales") const path = require(path) const slash = require(slash`)
const decode = require("unescape")
const crypto = require("crypto")

// Templates
const postTemplate = path.resolve(./src/templates/singleBlog.js)
const projectsTemplate = path.resolve(./src/templates/singleProject.js)
const projectCategoriesTemplate = path.resolve(./src/templates/projectCategory.js)
const categoriesTemplate = path.resolve(./src/templates/singleCategory.js)
const pageTemplate = path.resolve(./src/templates/singlePage.js)
const serviceTemplate = path.resolve(./src/templates/singleService.js)

// Components that need url override
const pageI18n = {
ComponentCompany: {
el: "/kataskevi-istoselidwn-eshop-thessaloniki/",
en: "/en/website-development-eshop-thessaloniki/"
}
}

// Polylang doesnt allow same slug for 2 different language for custom post types ( portfolio etc )
// Hack: For those cases we add a "-$locale" at the end of url. For example /company , /company-en and remove it here
const slugResolver = (lang, slug) => {
if (process.env.GATSBY_HEADLESS_REMOVE_LANG_PREFIX_FROM_PATHS) {
if (slug.endsWith("-" + lang)) {
return slug.replace(new RegExp("-" + lang + "$"), "")
}
}
return slug
}

// Find The right template
// @todo: refactor this shit later
const templateResolver = (template) => {
template = template ? template.toLowerCase() : template

switch (template) {
case ("services"):
return { template: serviceTemplate, prefix: "services" }

default:
  return { template: pageTemplate, prefix: null }

}
}

// Will return /prefix for English and / for default lang

const urlResolver = (lang) => {

return locales[lang].default ? "" : locales[lang].urlPrefix
}

exports.createPages = async ({ graphql, actions }) => {
const { createPage } = actions

// For Wordpress posts
const posts = await graphql(`
{
allWordpressPost {
edges {
node {
id
wordpress_id
slug
polylang_current_lang
title
path

  }
}

}
allWordpressWpProjects {
edges {
node {
id
slug
wordpress_id
polylang_current_lang
project_categories
acf {
display_image {
wordpress_id
}
technology_icons {
wordpress_id
}
}
}
}
}
allWordpressCategory {
edges {
node {
slug
path
name
polylang_current_lang
wordpress_id
}
}
}
}

`)

const projectCategories = await graphql(`
{
allWordpressWpProjectCategories {
edges {
node {

    wordpress_id
    id
    name
    slug
    polylang_current_lang
  }
}

}
}

`)

// For Wordpress Pages
const pages = await graphql(`
{
allWordpressPage {
edges {
node {
id
acf {
page_template
}
title
content
excerpt
slug
status
path
wordpress_id
polylang_current_lang
template

              }
          }
      }

  }

`)

// Check for any errors
// @todo add a function for this later

if (pages.errors) {
throw new Error(pages.errors)
}

if (posts.errors) {
throw new Error(posts.errors)

}

const { allWordpressPage } = pages.data

const { allWordpressPost } = posts.data
const { allWordpressWpProjects } = posts.data
const { allWordpressWpProjectCategories } = projectCategories.data
const { allWordpressCategory } = posts.data

// For each Post - Create a Page with /blog/slug
// We gonna map them by Locale so we can assign Next or Prev items based on Lang
let postMappedByLocale = {}

allWordpressPost.edges.forEach((edge) => {

// if Locale is already set at Object
if (postMappedByLocale[edge.node.polylang_current_lang]) {
  postMappedByLocale[edge.node.polylang_current_lang].push(edge)

} else { // Else Create the key first

  let tmpObj = {
    [edge.node.polylang_current_lang]: [],
  }
  postMappedByLocale = { ...postMappedByLocale, ...tmpObj }
  postMappedByLocale[edge.node.polylang_current_lang].push(edge)
}

})

console.debug("[Creating WP Posts]")

// Loop through all mapped Posts and Create Em
for (let langKey in postMappedByLocale) {

let langPrefix = urlResolver(langKey)
let slug
postMappedByLocale[langKey].forEach((edge, index) => {
  slug = slugResolver(edge.node.polylang_current_lang, edge.node.slug,false)
  createPage({
    path: `${langPrefix}/blog/${slug}/`,
    component: slash(postTemplate),
    context: {
      id: edge.node.wordpress_id,
      slug: slug,
      slugUnEscaped: edge.node.slug,
      locale: edge.node.polylang_current_lang,
      prev: index === 0 ? null : slugResolver(postMappedByLocale[langKey][index - 1].polylang_current_lang, postMappedByLocale[langKey][index - 1].node.slug,false), // If the Loop Index isnt 0
      next: index === (postMappedByLocale[langKey].length - 1) ? null : slugResolver(postMappedByLocale[langKey][index + 1].polylang_current_lang, postMappedByLocale[langKey][index + 1].node.slug,false), // If loop Index isnt the lenght of Array
    },
  })
})

}

console.debug("[Creating WP Post Categories]")

allWordpressCategory.edges.forEach((edge) => {
let langPrefix = urlResolver(edge.node.polylang_current_lang)
let slug
// Polylang doesnt allow same slug for different languages
// A quick workout is to add the same slug with the lang prefix in the end
// For example : /category/shop -> /category/shop-en
// In Gatsby we can check if locale is in the end and remove it

slug = slugResolver(edge.node.polylang_current_lang, edge.node.slug,false)

createPage({
  path: `${langPrefix}/category/${slug}/`,
  component: slash(categoriesTemplate),
  context: {
    id: edge.node.wordpress_id,
    slug: slug,
    slugUnEscaped: edge.node.slug,
    locale: edge.node.polylang_current_lang,

  },
})

})

// For Each pages - Create a page with /slug

console.debug("[Creating WP Pages]")
allWordpressPage.edges.forEach(edge => {

// Override them if it is a service and add the Service Template
let templateObj = templateResolver(edge.node.acf.page_template)
let slug
let pageTmpl = templateObj.template

// noinspection JSUnresolvedVariable
slug = slugResolver(edge.node.polylang_current_lang, edge.node.slug,false)
let path = templateObj.prefix ? templateObj.prefix + "/" + slug : slug
let langPrefix = urlResolver(edge.node.polylang_current_lang)


// Create Page
createPage({
  path: `${edge.node.path}`,
  component: slash(pageTmpl),
  context: {
    id: edge.node.wordpress_id,
    locale: edge.node.polylang_current_lang,
    tmpl: edge.node.template,
  },
})

})

let portfolioMappedByLocale = {}

// For each Portfolio - Create a Page with /portfolio/slug
// We gonna map them by Locale so we can assign Next or Prev items based on Lang
allWordpressWpProjects.edges.forEach(edge => {

// if Locale is already set at Object
if (portfolioMappedByLocale[edge.node.polylang_current_lang]) {
  portfolioMappedByLocale[edge.node.polylang_current_lang].push(edge)

} else { // Else Create the key first

  let tmpObj = {
    [edge.node.polylang_current_lang]: [],
  }
  portfolioMappedByLocale = { ...portfolioMappedByLocale, ...tmpObj }
  portfolioMappedByLocale[edge.node.polylang_current_lang].push(edge)
}

})

console.debug("[Creating WP Portfolio]")
for (let langKey in portfolioMappedByLocale) {

let langPrefix = urlResolver(langKey)
let slug
portfolioMappedByLocale[langKey].forEach((edge, index) => {
  slug = slugResolver(edge.node.polylang_current_lang, edge.node.slug,false)

  createPage({
    path: `${langPrefix}/portfolio/${slug}/`,
    component: slash(projectsTemplate),
    context: {
      id: edge.node.wordpress_id,
      slug: slug,
      slugUnEscaped: edge.node.slug,
      locale: edge.node.polylang_current_lang,
      devicesImageId: edge.node.acf.display_image != null && edge.node.acf.display_image.wordpress_id != null ? parseInt(edge.node.acf.display_image.wordpress_id) : 0,
      categories: edge.node.project_categories != null ? edge.node.project_categories : [],
      technologyIcons: edge.node.acf.technology_icons != null ? edge.node.acf.technology_icons.map((icon) => parseInt(icon.wordpress_id)) : [],
      prev: index === 0 ? null : slugResolver(portfolioMappedByLocale[langKey][index - 1].node.polylang_current_lang, portfolioMappedByLocale[langKey][index - 1].node.slug,false), // If the Loop Index isnt 0
      next: index === (portfolioMappedByLocale[langKey].length - 1) ? null : slugResolver(portfolioMappedByLocale[langKey][index + 1].node.polylang_current_lang, portfolioMappedByLocale[langKey][index + 1].node.slug,false), // If loop Index isn't eq to the length of Array

    },
  })
})

}

// console.debug("[Creating WP Portfolio Categories]")
// allWordpressWpProjectCategories.edges.forEach(edge => {
// let langPrefix = urlResolver(edge.node.polylang_current_lang)
//
// createPage({
// path: ${langPrefix}/project-category/${edge.node.slug}/,
// component: slash(projectCategoriesTemplate),
// context: {
// id: edge.node.wordpress_id,
// slug: edge.node.slug,
// locale: edge.node.polylang_current_lang,
//
// },
// })
// })

}
console.debug("[Creating Gatsby Static Pages]")
// Override the page creation and add the i18n version too
// Also add the locale as PageContext so we can query it with graphQL later
exports.onCreatePage = ({ page, actions }) => {
const { createPage, deletePage } = actions

return new Promise(resolve => {
deletePage(page)
// Foreach Language we have

  Object.keys(locales).map(lang => {

    // If language is the default one, dont add any prefix ( etc. /el/ )

    let localizedPath = locales[lang].default
      ? page.path
      : locales[lang].path + page.path

    // if we override the page path
    if(pageI18n.hasOwnProperty(page.internalComponentName)) {
      // localizedPath = pageI18n[page.internalComponentName][lang]
      return createPage({
        ...page,
        path: pageI18n[page.internalComponentName][lang],
        context: {
          locale: lang,
        },
      })
    }else {
      return createPage({
        ...page,
        path: localizedPath,
        context: {
          locale: lang,
        },
      })
    }

  })



resolve()

})
}

```

@foja123 what translation plugin do you use? can you send the code from your gatsby-node?

Hi this is my gatsby-node file... im using WP REST - Polylang, WPML REST API, WPML to Polylang, Polylang... with this plugin i have created my two pages.. one for it and one for en...

Where i have :
path: /LANG/${edge.node.slug}/,
i think i need a variable LANG for the language it o en.... but i dont how to take it

--- gatsby- node------

const _ = require(lodash)
const Promise = require(bluebird)
const path = require(path)
const slash = require(slash)

// Implement the Gatsby API “createPages”. This is
// called after the Gatsby bootstrap is finished so you have
// access to any information necessary to programmatically
// create pages.
// Will create pages for WordPress pages (route : /{slug})
// Will create pages for WordPress posts (route : /post/{slug})
exports.createPages = ({ graphql, actions }) => {
const { createPage, createRedirect } = actions

createRedirect({ fromPath: '/', toPath: '/it/home', redirectInBrowser: true, isPermanent: true })
return new Promise((resolve, reject) => {
// The “graphql” function allows us to run arbitrary
// queries against the local WordPress graphql schema. Think of
// it like the site has a built-in database constructed
// from the fetched data that you can run queries against.

// ==== PAGES (WORDPRESS NATIVE) ====
graphql(
  `
    {
      allWordpressPage {
        edges {
          node {
            id
            slug
            status
            template
            title
            content
            polylang_translations {
              title
              content
              id
            }
            acf {
              photo_gallery {
                image_gallery {
                  wordpress_id
                  title
                  caption
                  thumbnail_image_url
                  large_srcset
                  medium_srcset
                  url
                  target
                }
              }
              image_viewer {
                source_url
                caption
              }
              image_viewer_mobile {
                source_url
              }
            }
          }
        }
      }
    }
  `
)
  .then(result => {
    if (result.errors) {
      console.log(result.errors)
      reject(result.errors)
    }

    // Create Page pages.
    const pageTemplate = path.resolve("./src/templates/page.js")
    const pageTemplateHome = path.resolve("./src/templates/pageHome.js")
    // We want to create a detailed page for each
    // page node. We'll just use the WordPress Slug for the slug.
    // The Page ID is prefixed with 'PAGE_'
    _.each(result.data.allWordpressPage.edges, edge => {
      // Gatsby uses Redux to manage its internal state.
      // Plugins and sites can use functions like "createPage"
      // to interact with Gatsby.

      let comp = slash(pageTemplate);
      if(edge.node.slug === 'home') {
        comp = slash(pageTemplateHome);
      }

      createPage({
        // Each page is required to have a `path` as well
        // as a template component. The `context` is
        // optional but is often necessary so the template
        // can query data specific to each page.
        path: `/it/${edge.node.slug}/`,
        component: comp,
        context: edge.node,

    })
    })
  })
  // ==== END PAGES ====

  // ==== POSTS (WORDPRESS NATIVE AND ACF) ====
  .then(() => {
    graphql(
      `
        {
          allWordpressPost {
            edges{
              node{
                id
                title
                slug
                excerpt
                content
              }
            }
          }
        }
      `
    ).then(result => {
      if (result.errors) {
        console.log(result.errors)
        reject(result.errors)
      }
      const postTemplate = path.resolve("./src/templates/post.js")
      // We want to create a detailed page for each
      // post node. We'll just use the WordPress Slug for the slug.
      // The Post ID is prefixed with 'POST_'
      _.each(result.data.allWordpressPost.edges, edge => {
        createPage({
          path: `/post/${edge.node.slug}/`,
          component: slash(postTemplate),
          context: edge.node,
        })
      })
      resolve()
    })
  })
// ==== END POSTS ====
// ==== POSTS PLACES (WORDPRESS NATIVE AND ACF) ====
.then(() => {
  graphql(
    `
      {
        allWordpressWpPlaces {
          edges {
              node {
                  id
                  slug
                  title
                  content
                  featured_media {
                    source_url
                  }
              }
          }
      }
      }
    `
  ).then(result => {
    if (result.errors) {
      console.log(result.errors)
      reject(result.errors)
    }
    const placeTemplate = path.resolve("./src/templates/places.js")
    // We want to create a detailed page for each
    // post node. We'll just use the WordPress Slug for the slug.
    // The Post ID is prefixed with 'POST_'
    _.each(result.data.allWordpressWpPlaces.edges, edge => {
      createPage({
        path: `/places/${edge.node.slug}/`,
        component: slash(placeTemplate),
        context: edge.node,
      })
    })
    resolve()
  })
})

// ==== END POSTS PLACES ====
})
}

Thanks

i dont really get why you are using both polylang and wpml. if you use polylang for the translation in your wordpress you should be able to include the lang inside the graphql query like that:

    {
      allWordpressPage {
        edges {
          node {
            id
            slug
            status
            template
            title
            lang
          }
        }
      }
    }

now that you have lang you can use it as edge.node.lang inside your _each function and this will replace your LANG placeholder

as a sidenode you dont need all that other stuff e.g acf in this query because you don´t use it in the gatsby-node and only need this inside the page template itself

@arturhenryy i deleted wpml plugin but i still cant have lang in allWordpressPage..
And why i cant have polylang_current_lang in allWordpressPage and i have only in allWordpressPost? How did you implement the menu to switch the language?

Was this page helpful?
0 / 5 - 0 ratings

Related issues

KyleAMathews picture KyleAMathews  Âˇ  3Comments

hobochild picture hobochild  Âˇ  3Comments

kalinchernev picture kalinchernev  Âˇ  3Comments

rossPatton picture rossPatton  Âˇ  3Comments

signalwerk picture signalwerk  Âˇ  3Comments