Creating Translation Strings

This section will show you how to make your panel's frontend texts translatable.

Whether it's a button label, a page title, or a status message. We will guide you step by step through the process of creating and managing translation strings in your addon.

You will learn how to:

  • Add language keys to your lang/ directory

  • Replace hardcoded text in components with translatable strings

  • Support multiple languages in a clean and scalable way

No advanced coding skills are required. We will keep it simple and practical for everyone!

Step 1: Prepare your language directory

Before you can start creating translation strings, you need to prepare the language folder where you addon will store its translations.

Pterodactyl comes with a default language directory that already includes some translations, but not all strings are used in the panel.

To create your own translations directory, you will need to access the resources/lang directory inside the panel's main installation folder.

cd /var/www/pterodactyl/resources/lang

To create a new translation strings folder for your language, copy the existing en folder and rename it using the correct language code (for example: ro for Romanian, de for German, es for Spanish and so on)

cp -r en ro

Replace ro with your desired language code.

I haven't personally verified all of the default files provided by Pterodactyl, so I cannot guarantee which ones are actively used.

However, one file that is confirmed to be used is activity.php

It contains important activity log messages that will appear in the activity page, and I highly recommend translating that file.

Organizing your translation files (Namespaces)

After you've created your language folder (e.g. resources/lang/ro), you will notice that the structure may include subfolders like:

  • dashboard

  • server

  • admin

These subfolders are called namespaces. They help organize translation strings based on where they are used in the panel (e.g. dashboard page, account page, server page, etc.)

Each namespace contains one or more .php files with grouped translation keys.

Importing useTranslation in your React Component:

In any React component where you want to use translations:

import { useTranslation } from 'react-i18next';

Initializing the translation with a namespace

Each group of translations belongs to a namespace (based on your translation file path).

Example:

const { t } = useTranslation('dashboard/index');
  • dashboard/index refers to this file

/resources/lang//dashboard/index.php

If your translation file is directly in the lang/ folder:

const { t } = useTranslation('activity');

This refers to:

/resources/lang//activity.php

Writing translations

In your translation file, return a PHP array where:

  • keys are the string you will use in t('key')

  • values are the actual translated strings

Example: /resources/lang/ro/dashboard/index.php

<?php

return [
    // other strings
    'show_your_servers' => 'Arată serverele tale',
    'show_other_servers' => 'Arată alte servere',
];

Replacing strings in components

Before:

{showOnlyAdmin ? 'Showing others servers' : 'Showing your servers'}

After:

{showOnlyAdmin ? t('show_other_servers') : t('show_your_servers')}

You just replaced two strings with translation keys, making your app multilingual.

Best Practice for Translation Keys

Translation keys shoud be:

  • Unique

  • Descriptive

  • Without spaces or special characters

✅ Good:

'show_other_servers' => 'Arată alte servere',

🚫 Avoid:

'show other servers' => 'Arată alte servere',

Recap with full example

Language file: /resources/lang/XX/dashboard/index.php

<?php

return [
    'show_your_servers' => 'Arată serverele tale',
    'show_other_servers' => 'Arată alte servere',
];

React component:

import React, { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Server } from '@/api/server/getServer';
import getServers from '@/api/getServers';
import ServerRow from '@/components/dashboard/ServerRow';
import Spinner from '@/components/elements/Spinner';
import PageContentBlock from '@/components/elements/PageContentBlock';
import useFlash from '@/plugins/useFlash';
import { useStoreState } from 'easy-peasy';
import { usePersistedState } from '@/plugins/usePersistedState';
import Switch from '@/components/elements/Switch';
import tw from 'twin.macro';
import useSWR from 'swr';
import { PaginatedResult } from '@/api/http';
import Pagination from '@/components/elements/Pagination';
import { useLocation } from 'react-router-dom';

export default () => {
    const { search } = useLocation();
    const defaultPage = Number(new URLSearchParams(search).get('page') || '1');

    const [page, setPage] = useState(!isNaN(defaultPage) && defaultPage > 0 ? defaultPage : 1);
    const { clearFlashes, clearAndAddHttpError } = useFlash();
    const uuid = useStoreState((state) => state.user.data!.uuid);
    const rootAdmin = useStoreState((state) => state.user.data!.rootAdmin);
    const [showOnlyAdmin, setShowOnlyAdmin] = usePersistedState(`${uuid}:show_all_servers`, false);

    const { data: servers, error } = useSWR<PaginatedResult<Server>>(
        ['/api/client/servers', showOnlyAdmin && rootAdmin, page],
        () => getServers({ page, type: showOnlyAdmin && rootAdmin ? 'admin' : undefined })
    );

    useEffect(() => {
        if (!servers) return;
        if (servers.pagination.currentPage > 1 && !servers.items.length) {
            setPage(1);
        }
    }, [servers?.pagination.currentPage]);

    useEffect(() => {
        // Don't use react-router to handle changing this part of the URL, otherwise it
        // triggers a needless re-render. We just want to track this in the URL incase the
        // user refreshes the page.
        window.history.replaceState(null, document.title, `/${page <= 1 ? '' : `?page=${page}`}`);
    }, [page]);

    useEffect(() => {
        if (error) clearAndAddHttpError({ key: 'dashboard', error });
        if (!error) clearFlashes('dashboard');
    }, [error]);

    const { t } = useTranslation('dashboard/index');

    return (
        <PageContentBlock title={t('dashboard')} showFlashKey={'dashboard'}>
            {rootAdmin && (
                <div css={tw`mb-2 flex justify-end items-center`}>
                    <p css={tw`uppercase text-xs text-neutral-400 mr-2`}>
                        {showOnlyAdmin ? t('show_other_servers') : t('show_your_servers')}
                    </p>
                    <Switch
                        name={'show_all_servers'}
                        defaultChecked={showOnlyAdmin}
                        onChange={() => setShowOnlyAdmin((s) => !s)}
                    />
                </div>
            )}
            {!servers ? (
                <Spinner centered size={'large'} />
            ) : (
                <Pagination data={servers} onPageSelect={setPage}>
                    {({ items }) =>
                        items.length > 0 ? (
                            items.map((server, index) => (
                                <ServerRow key={server.uuid} server={server} css={index > 0 ? tw`mt-2` : undefined} />
                            ))
                        ) : (
                            <p css={tw`text-center text-sm text-neutral-400`}>
                                {showOnlyAdmin
                                    ? 'There are no other servers to display.'
                                    : 'There are no servers associated with your account.'}
                            </p>
                        )
                    }
                </Pagination>
            )}
        </PageContentBlock>
    );
};

If you don't see your translations

Make sure:

  • You're using the correct namespace

  • That translation file exists

  • The key exists in the file

  • You have rebuilt your frontend assets after adding new translation files or keys:

Last updated