Skip to content

Translations

Updated

PNLCS ships in 30 languages. English, Turkish, Polish, Chinese and German are complete, and the test suite keeps them complete. The other 25 are community languages: they are translated as people contribute, and any text a community language does not have yet is shown in English - never as a raw key, and never as a broken page. Every language you improve reaches every PNLCS install.

Where the texts live

lang/
├── en/            the source: every text exists here first
│   ├── admin.php
│   ├── client.php
│   ├── common.php
│   └── ...
├── de/            one folder per language, the same file names
└── ...

lang/en/ has 20 files: aapanel, admin, auth, client, common, email, errors, group, install, messages, pagination, panelica_license, panelica, passwords, pdf, proxmox, reports, sections, validation and whmcs_import. The complete languages (and Spanish, Russian and Ukrainian) have all 20; most community languages have six (admin, client, common, email, proxmox, whmcs_import), and every text of a missing file is shown in English. Laravel's own validation messages are read first from the framework, so a rule without a line in lang/<code>/validation.php still shows a sentence.

Each file returns an array of key => text. Keys are either nested or written flat with dots; both are read the same way:

return [
    'dashboard' => ['welcome' => 'Welcome back, :name'],   // nested
    'dashboard.welcome_back' => 'Welcome back',            // flat
];

When the same key exists both ways, the flat one wins

If a file has 'dashboard.welcome' => '...' and also 'dashboard' => ['welcome' => '...'], the flat line is the one shown. Edit the line that is actually used, or remove the duplicate.

Every text the product shows lives in these files. Texts an operator edits on Setup → Languages are stored in the database and take priority over the files, so they survive updates; nothing the project ships is kept in the database.

Rules every translation follows

  • Keep every placeholder exactly: :name, :count, :amount and the like are filled in by the code.
  • Keep HTML the English text has (<strong>, <code>, links) in the same order.
  • Keep the form of address the language already uses everywhere: German and Turkish address the reader formally (Sie, siz), Polish informally.
  • Product and protocol names (PNLCS, SMTP, DNS, IBAN) stay as they are.

What the tests check

Run them before you send anything: php artisan test --filter=Translation.

Every language, complete or community:

  • each file loads and returns its texts;
  • every placeholder the English text has is in the translation;
  • the translation has exactly the tags the English has - no new tags, no new attributes;
  • no text is blank (a missing text falls back to English, a blank one shows nothing).

The complete languages (Turkish, German, Polish, Chinese) also must have every English key, in the same order as English, and may leave a text identical to English only when it is listed (below). A pull request that adds an English text writes it in these four too, or says in the description which ones it could not.

Community languages are not required to have every key. The tests print how many texts each one still shows in English; that number is a to-do list, not a reason to fail.

Texts that stay the same as English

Some texts are the same in a language on purpose: product names (Stripe, PayPal, phpMyAdmin), protocols and units (Port, TTL, :value vCPU), and words the language spells the same (German Status, Domain, Name). The translation progress on Setup → Languages counts a text that is identical to English as not yet translated - that is how it tells a translated language from a copy of the English - so these are listed one by one in database/data/same_as_english.php:

'de' => [
    'admin.clients.status',     // Status
    'client.hosting.dns.zone',  // Zone
],

A text with no words in it (SSL, :count, %) needs no line. For a complete language the list must be exact: a text identical to English that is not listed fails the tests, and so does a listed text that has since been translated. A community language may add its own section; its lines are only checked for still being true.

When a language becomes complete

When a community language has every English key and every text identical to English is listed, open a pull request that says so. A maintainer moves it from the community list to the complete list in tests/Feature/TranslationParityTest.php, after which new English texts have to be written in it as well.

Send a translation

Either way works:

  • A pull request changing lang/<code>/*.php. Your name stays on it in the project history, and you are listed under Translators in the README. The translation tests must pass: php artisan test --filter=Translation.
  • An export from your own install. On Setup → Languages → Translate → Export JSON, translate the file, and send it to [email protected] or attach it to a GitHub issue. We review it, merge it and credit you by name, in the changelog and under Translators in the README.

From an install into the files

Texts saved in the editor or produced by Translate with AI live in that install's database. To turn them into a contribution, run on a checkout of the repository:

php artisan pnlcs:lang-write tr --dry-run   # what would change
php artisan pnlcs:lang-write tr             # write into lang/tr/*.php
php artisan pnlcs:lang-write tr --clear     # and drop the rows now in the files

Only the values that differ from the files are written, in place: comments, order and layout stay as they are, so the diff shows just the changed texts. A key the file does not have yet is added at its end. Use --group=admin for one file. Then run the translation tests, commit and open a pull request.

Email templates

The email templates (Setup → Email Templates) are database rows, one set per language, which the operator edits. A language starts from the set PNLCS ships for it in database/data/email_templates/<code>.php, template name => subject and message; a language without that file starts from the English set. German and Polish are shipped today.

return [
    'Invoice Created' => [
        'subject' => 'Neue Rechnung #{invoice_num} – {CompanyName}',
        'message' => "Guten Tag {client_name},\n\n...",
    ],
];
  • Keep exactly the English template's merge fields ({client_name}, {invoice_num} ...); {CompanyName} may be added, every email has it. tests/Feature/ShippedEmailTemplatesTest.php checks this.
  • The subject line of a template goes out with every email, so a template translation is what gives a customer's email a subject in their language.
  • An install that already has the language gets a new template translation by migration, and only where the row is still the English text: anything the operator changed stays.

A new language

Open an issue first: a language is added to the list the installer seeds and to the tests that measure coverage, and a partial translation needs a plan to become complete.