Docs

Learn

v.Latest
Documentation
Internationalization
Table of Contents

Internationalization with TinaCMS

Managing multilingual content is essential for global reach. TinaCMS provides versatile options to facilitate this. This guide focuses on two main strategies:

  • Directory-Based Localization
  • Field-Based Localization

Directory-Based Localization

With directory-based localization, your content should be structured in a directory-based manner, where the locale (e.g., en, fr) lives underneath the collection root (e.g., blog, docs). For example:

.
ā”œā”€ā”€ /blog/
ā”‚ ā”œā”€ā”€ /en/
ā”‚ ā”‚ ā””ā”€ā”€ hello-world.md
ā”‚ ā””ā”€ā”€ /fr/
ā”‚ ā””ā”€ā”€ hello-world.md
ā””ā”€ā”€ /docs/
ā”œā”€ā”€ /en/
ā”‚ ā””ā”€ā”€ my-doc.md
ā””ā”€ā”€ /fr/
ā””ā”€ā”€ my-doc.md
Steps to Implement Directory-Based Localization

In your config.ts, you likely will have one collection that contains all your locales

export const config = {
collections: [
{
label: 'Blog',
name: 'blog',
path: 'content/blog',
// ...other settings
},
}
Routing and File Structure:

Whether you're using Next.js, or another framework, your routing logic should be updated to pick the correct locale based on either the URL or user setting.

// Example: Fetching the page list in NextJS
const getStaticPaths = async({ locales }) {
// ...
})

Using the locale, you can filter for document(s) based on the path

// /pages/post/[filename].tsx
// `locale` is provided alongside `params`
const getStaticProps = async({ params, locale }) {
const tinaProps = await client.BlogPostQuery({
// compose `relativePath` where `locale` is a sub-folder to the `post`
relativePath: `${locale}/${params.filename}.mdx`,
});
return {
props: {
...tinaProps
}
}
}
For more info on setting up the routing for NextJS-specific implementations, see our guide
Content Management

With this setup, editors will browse locales for each collection via the document list.

Localized List

If a user wants to create a new localized version of an existing document, they can click "duplicate document" from the document list, and prepend the desired locale in the new document's filename.

Field-Based Localization

In this approach, each localized field contains nested values for multiple languages. For example, a single Markdown file might look like this:

{
"title": {
"en": "Hello",
"fr": "Bonjour"
}
}
Steps to Implement Field-Based Localization

You will need to modify your TinaCMS schema to include localized fields.

export const pageSchema = {
label: 'Page',
name: 'page',
fields: [
{
label: 'Title',
name: 'title',
type: 'object',
fields: [
{
type: 'string',
name: 'en',
label: 'English',
},
{
type: 'string',
name: 'fr',
label: 'French',
},
],
},
// ...other fields
],
};
Note: If you are using markdown/mdx content, and want to use the markdown body for your content, you might prefer using the directory-based approach to localization.
Display Localized Content:

In your site's components, you can then choose the correct localized field to display based on the current locale.

const PageComponent = ({ data, locale }) => {
const title = data.title[locale];
// ...display content
};
Content Management:

TinaCMS will display all localized fields as children of the root-level field.

Localized Fields

Internationalization with GitHub Action

When using Directory-Based Localization, developers need to place the corresponding language MDX or JSON files in the correct location for loading multilingual routes. Developers can choose to manually translate text content and place it in the correct location, or they can use a GitHub Action to automate this process. The following is an example explaining the workflow of an effective GitHub Action:

Auto Translation Workflow

Setting up trigger conditions for GitHub Actions

A trigger condition must be set for this GitHub Action. Generally, triggering occurs when a PR is successfully merged. To reduce token consumption, scheduled triggering is an alternative. The example below shows a GitHub Action that activates only when a PR is merged with changes in the specified directory.

# Only triggered after a PR is successfully closed and contains changes in the specified directory
on:
pull_request:
types: [closed]
paths:
- 'content/docs/**.mdx'
jobs:
translate-mdx:
if: github.event.pull_request.merged == true

Identify modified target files

To minimize token usage, identifying only modified files for translation is essential. Below is a simple example of listing all the modified files.

// Identify modified target files under currrent PR
async function getChangedFilesFromApi() {
try {
const response = await axios.get(
`https://api.github.com/repos/${OWNER}/${REPO_NAME}/pulls/${PR_NUMBER}/files`,
{
headers: {
Authorization: `token ${GITHUB_TOKEN}`,
Accept: 'application/vnd.github.v3+json',
},
}
);
const mdxFiles = response.data
.filter(
(file) =>
file.filename.startsWith('content/docs/') &&
file.filename.endsWith('.mdx')
)
.map((file) => file.filename);
return mdxFiles;
} catch (error) {
console.error('Error fetching changed files from API:', error.message);
return [];
}
}

Create Translation Branch

Creating a separate branch to differentiate between the original branch and newly added translations is essential. This approach facilitates a manual review.

# Create a separate branch for translated files
- name: Create translation branch
if: env.HAS_CHANGED_FILES == 'true'
run: |
TIMESTAMP=$(date +%s)
PR_NUMBER="${{ github.event.pull_request.number }}"
BRANCH_NAME="translate-pr-$PR_NUMBER-$TIMESTAMP"
git checkout -b $BRANCH_NAME
echo "BRANCH_NAME=$BRANCH_NAME" >> $GITHUB_ENV

Acquiring translations through LLM API Calls

External LLMs are employed for automatic translation. Translation quality and format integrity depend on the selected model and the prompt. Below is an English prompt example for translating MDX files into Chinese while preserving document interfaces and critical information.

Please translate the following Markdown content into Chinese, preserving all Markdown formatting, code blocks, and front matter metadata.Only translate the text content, do not modify code, variable names, or other technical content.Do not add ```markdown``` in the newly translated Markdown file.Please note the following special requirements:
1. Do not modify URLs, next and preview fields
2. Maintain the original format structure, including heading hierarchy, lists, tables, etc.
3. Comments in code blocks can be translated, but the code itself and its functionality should not be changed
4. Please return the translated markdown source code directly in your reply, without adding any other prompts or explanations
{{content}}

Commit all changes to the new branch and create a PR

Content generated by LLMs cannot be considered fully reliable. Committing all changes to a new PR for thorough verification represents a more cautious and methodical approach.

const prTitle = `Chinese translation for PR #${PR_NUMBER}`;
const prBody = `This PR contains Chinese translations for the documentation files updated in PR (${SERVER_URL}/${REPO}/pull/${PR_NUMBER}).`;
const response = await axios.post(
`https://api.github.com/repos/${OWNER}/${REPO_NAME}/pulls`,
{
title: prTitle,
body: prBody,
head: BRANCH_NAME,
base: 'main',
},
{
headers: {
Authorization: `token ${GITHUB_TOKEN}`,
Accept: 'application/vnd.github.v3+json',
},
}
);
Last Edited: April 14, 2025

Join the Herd!

Become part of our coding comunity and stay updated with the latest tips and news.