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
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
Note: Not all of the translation strings found inside the language folders are actually used in the user interface. Some of them may be related to internal logic or unused messages.
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:
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