Hello everyone!
I fell in love with Sitely — it reminds me of the old, secure way of building websites… but without the need for coding (aside from language-specific logic). The only drawback is that the Preview function doesn’t support PHP execution. To see PHP in action, you’ll need to publish your site on a PHP-enabled server, and you’ll also need a Sitely Developer License.
P.S.: I’m a new user, so I can’t upload screenshots yet.
Here’s a quick tutorial on how to create a multi-language website without duplicating all the pages or redoing the formatting for different screen sizes:
1. make sure all pages you create use .php extension
2. create on your server those files :
public_html/
├── ln/
│ ├── languages/
│ │ ├── en.json
│ │ ├── fr-ca.json etc..
│ ├── assets/
│ │ ├── dropdown-arrow.svg (you can create or upload any svg arrow)
│ ├── set_language.php
│ ├── language_handler.php (test page independent of Sitely)
│ ├── language_init.php
3. the language json files look like this :
{
"EssayezLe": "Try it",
"buttons": {
"submit": "Submit",
"cancel": "Cancel"
},
"errors": {
"required_field": "This field is required.",
"invalid_email": "Please enter a valid email address."
},
"welcome_message": "Welcome !"
}
In the language folder you can use inner arrays like buttons.submit or buttons.cancel for your placeholder language. We will see later on this tutorial.
4. here the code for set_language.php
<?php
header('Content-Type: application/json');
class Language {
private $translations = [];
private $lang;
private const ALLOWED_LANGUAGES = ['en', 'fr-ca'];
private const DEFAULT_LANGUAGE = 'fr-ca';
private const LANGUAGE_DIR = __DIR__ . '/languages/';
public function __construct(?string $lang = null) {
$this->lang = $this->validateLanguage($lang ?? self::DEFAULT_LANGUAGE);
$this->loadTranslations();
}
private function validateLanguage(string $lang): string {
if (in_array($lang, self::ALLOWED_LANGUAGES, true)) {
return $lang;
}
error_log("Invalid language provided: {$lang}. Defaulting to " . self::DEFAULT_LANGUAGE);
return self::DEFAULT_LANGUAGE;
}
private function loadTranslations(): void {
$file = self::LANGUAGE_DIR . "{$this->lang}.json";
if (!file_exists($file)) {
$file = self::LANGUAGE_DIR . self::DEFAULT_LANGUAGE . '.json';
$this->lang = self::DEFAULT_LANGUAGE;
error_log("Language file not found for {$this->lang}. Using default: " . self::DEFAULT_LANGUAGE);
}
$content = file_get_contents($file);
$data = json_decode($content, true);
$this->translations = json_last_error() === JSON_ERROR_NONE ? $data : [];
if (json_last_error() !== JSON_ERROR_NONE) {
error_log("Invalid JSON in language file: {$file}");
}
}
public function get(string $key, string $default = ''): string {
$keys = explode('.', $key);
$value = $this->translations;
foreach ($keys as $k) {
if (is_array($value) && isset($value[$k])) {
$value = $value[$k];
} else {
return $default;
}
}
return is_string($value) ? $value : $default;
}
public function getTranslations(): array {
return $this->translations;
}
public function getCurrentLanguage(): string {
return $this->lang;
}
}
$response = ['success' => false, 'error' => ''];
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['lang'])) {
$lang = is_string($_POST['lang']) ? $_POST['lang'] : null;
$language = new Language($lang);
$new_lang = $language->getCurrentLanguage();
// Set cookie with secure settings
setcookie('lang', $new_lang, [
'expires' => time() + (30 * 24 * 60 * 60),
'path' => '/',
'secure' => isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on',
'httponly' => false,
'samesite' => 'Strict'
]);
$response['success'] = true;
$response['lang'] = $new_lang;
$response['translations'] = [
'welcome_message' => $language->get('welcome_message', 'Welcome!'),
'buttons.submit' => $language->get('buttons.submit', 'Submit'),
'current_language' => $new_lang
];
} else {
$response['error'] = 'Invalid request';
}
echo json_encode($response);
exit;
5. the code for language_init.php
<?php
// Enable error logging
ini_set('log_errors', 1);
ini_set('error_log', __DIR__ . '/error.log');
ini_set('display_errors', 0);
class Language {
private $translations = [];
private $lang;
private const ALLOWED_LANGUAGES = ['en', 'fr-ca'];
private const DEFAULT_LANGUAGE = 'fr-ca';
private const LANGUAGE_DIR = __DIR__ . '/languages/';
public function __construct(?string $lang = null) {
try {
$this->lang = $this->validateLanguage($lang ?? $_COOKIE['lang'] ?? self::DEFAULT_LANGUAGE);
$this->loadTranslations();
} catch (Exception $e) {
error_log("Language constructor error: " . $e->getMessage());
$this->lang = self::DEFAULT_LANGUAGE;
$this->translations = [];
}
}
private function validateLanguage(string $lang): string {
if (!in_array($lang, self::ALLOWED_LANGUAGES, true)) {
error_log("Invalid language: {$lang}. Defaulting to " . self::DEFAULT_LANGUAGE);
return self::DEFAULT_LANGUAGE;
}
return $lang;
}
private function loadTranslations(): void {
$file = self::LANGUAGE_DIR . "{$this->lang}.json";
if (!file_exists($file)) {
$file = self::LANGUAGE_DIR . self::DEFAULT_LANGUAGE . '.json';
$this->lang = self::DEFAULT_LANGUAGE;
if (!file_exists($file)) {
error_log("Language file not found: {$file}");
$this->translations = [];
return;
}
}
if (!is_readable($file)) {
error_log("Language file not readable: {$file}");
$this->translations = [];
return;
}
$content = @file_get_contents($file);
if ($content === false) {
error_log("Failed to read language file: {$file}");
$this->translations = [];
return;
}
$data = json_decode($content, true);
if (json_last_error() !== JSON_ERROR_NONE) {
error_log("Invalid JSON in language file: {$file}. Error: " . json_last_error_msg());
$this->translations = [];
return;
}
$this->translations = $data;
}
public function get(string $key, string $default = ''): string {
$keys = explode('.', $key);
$value = $this->translations;
foreach ($keys as $k) {
if (is_array($value) && isset($value[$k])) {
$value = $value[$k];
} else {
return $default;
}
}
return is_string($value) ? $value : $default;
}
public function getCurrentLanguage(): string {
return $this->lang;
}
}
// Initialize language globally
global $language;
$language = new Language();
?>
6. and finally the code for the test page language_handler.php
<?php
// Enable error logging
ini_set('log_errors', 1);
ini_set('error_log', __DIR__ . '/error.log');
ini_set('display_errors', 0); // Disable display for production
class Language {
private $translations = [];
private $lang;
private const ALLOWED_LANGUAGES = ['en', 'fr-ca'];
private const DEFAULT_LANGUAGE = 'fr-ca';
private const LANGUAGE_DIR = __DIR__ . '/languages/'; // Updated for ln/languages/
private const LANGUAGE_NAMES = [
'en' => 'English',
'fr-ca' => 'Français (CA)'
];
public function __construct(?string $lang = null) {
try {
$this->lang = $this->validateLanguage($lang ?? $_COOKIE['lang'] ?? self::DEFAULT_LANGUAGE);
$this->loadTranslations();
} catch (Exception $e) {
error_log("Language constructor error: " . $e->getMessage());
$this->lang = self::DEFAULT_LANGUAGE;
$this->translations = [];
}
}
private function validateLanguage(string $lang): string {
if (!in_array($lang, self::ALLOWED_LANGUAGES, true)) {
error_log("Invalid language: {$lang}. Defaulting to " . self::DEFAULT_LANGUAGE);
return self::DEFAULT_LANGUAGE;
}
return $lang;
}
private function loadTranslations(): void {
$file = self::LANGUAGE_DIR . "{$this->lang}.json";
if (!file_exists($file)) {
$file = self::LANGUAGE_DIR . self::DEFAULT_LANGUAGE . '.json';
$this->lang = self::DEFAULT_LANGUAGE;
if (!file_exists($file)) {
error_log("Language file not found: {$file}");
$this->translations = [];
return;
}
}
if (!is_readable($file)) {
error_log("Language file not readable: {$file}");
$this->translations = [];
return;
}
$content = @file_get_contents($file);
if ($content === false) {
error_log("Failed to read language file: {$file}");
$this->translations = [];
return;
}
$data = json_decode($content, true);
if (json_last_error() !== JSON_ERROR_NONE) {
error_log("Invalid JSON in language file: {$file}. Error: " . json_last_error_msg());
$this->translations = [];
return;
}
$this->translations = $data;
}
public function get(string $key, string $default = ''): string {
$keys = explode('.', $key);
$value = $this->translations;
foreach ($keys as $k) {
if (is_array($value) && isset($value[$k])) {
$value = $value[$k];
} else {
return $default;
}
}
return is_string($value) ? $value : $default;
}
public function getCurrentLanguage(): string {
return $this->lang;
}
public function getAvailableLanguageFiles(): array {
$files = glob(self::LANGUAGE_DIR . '*.json');
if ($files === false) {
error_log("Failed to glob language files in: " . self::LANGUAGE_DIR);
return [];
}
$result = [];
foreach ($files as $file) {
if (!is_readable($file)) {
error_log("Language file not readable: {$file}");
continue;
}
$filename = basename($file);
$lang_code = str_replace('.json', '', $filename);
$friendly_name = self::LANGUAGE_NAMES[$lang_code] ?? $lang_code;
$result[] = [
'filename' => $filename,
'friendly_name' => $friendly_name,
'supported' => in_array($lang_code, self::ALLOWED_LANGUAGES, true)
];
}
return $result;
}
public function getLanguagesFolderUrl(): string {
try {
$protocol = isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on' ? 'https' : 'http';
$host = $_SERVER['HTTP_HOST'] ?? 'localhost';
$dir = str_replace($_SERVER['DOCUMENT_ROOT'] ?? '', '', self::LANGUAGE_DIR);
$dir = str_replace('\\', '/', $dir); // Normalize for Windows
return rtrim("{$protocol}://{$host}{$dir}", '/');
} catch (Exception $e) {
error_log("Error calculating languages folder URL: " . $e->getMessage());
return 'Unable to determine folder URL';
}
}
}
// Initialize language
$init_error = '';
try {
$language = new Language();
$lang = $language->getCurrentLanguage();
$language_files = $language->getAvailableLanguageFiles();
$languages_folder_url = $language->getLanguagesFolderUrl();
} catch (Exception $e) {
error_log("Initialization failed: " . $e->getMessage());
$init_error = "Failed to initialize language system. Please check server logs.";
$lang = Language::DEFAULT_LANGUAGE;
$language = new Language();
$language_files = [];
$languages_folder_url = 'Error: Unable to determine folder URL';
}
?>
<!DOCTYPE html>
<html lang="<?php echo htmlspecialchars($lang, ENT_QUOTES, 'UTF-8'); ?>">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><?php echo htmlspecialchars($language->get('welcome_message', 'Welcome!'), ENT_QUOTES, 'UTF-8'); ?></title>
<!-- jQuery CDN with local fallback -->
<script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
<script>
window.jQuery || document.write('<script src="/ln/assets/jquery-3.7.1.min.js"><\/script>');
</script>
<style>
.language-form {
margin: 20px;
}
.language-select {
appearance: none;
-webkit-appearance: none;
-moz-appearance: none;
padding: 5px 5px 5px 5px;
font-size: 16px;
border: 1px solid #d1d5db;
border-radius: 6px;
background-color: #ffffff;
cursor: pointer;
background-image: url('/ln/assets/dropdown-arrow.svg');
background-repeat: no-repeat;
background-position: right 15px center;
background-size: 12px;
min-width: 150px;
transition: border-color 0.2s;
}
.language-select:focus {
outline: none;
border-color: #3b82f6;
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
}
.translation {
transition: opacity 0.3s ease;
}
.error-message {
color: #dc2626;
margin: 10px 20px;
display: none;
}
.init-error {
color: #dc2626;
margin: 10px 20px;
font-weight: bold;
}
.language-files {
margin: 20px;
padding: 10px;
border: 1px solid #e5e7eb;
border-radius: 4px;
max-width: 400px;
}
.language-files h3 {
margin: 0 0 10px 0;
font-size: 16px;
color: #1f2937;
}
.language-files ul {
list-style-type: none;
padding: 0;
margin: 0;
}
.language-files li {
padding: 5px 0;
color: #374151;
}
.language-files .supported {
color: #15803d;
font-weight: bold;
}
.language-files .unsupported {
color: #6b7280;
}
.language-files p {
margin: 10px 0;
font-size: 14px;
color: #4b5563;
}
.language-files .folder-url {
word-break: break-all;
font-family: monospace;
}
</style>
</head>
<body>
<?php if ($init_error): ?>
<p class="init-error"><?php echo htmlspecialchars($init_error, ENT_QUOTES, 'UTF-8'); ?></p>
<?php endif; ?>
<h1 class="translation" data-key="welcome_message"><?php echo htmlspecialchars($language->get('welcome_message', 'Welcome!'), ENT_QUOTES, 'UTF-8'); ?></h1>
<form class="language-form" id="languageForm">
<select class="language-select" name="lang" aria-label="Select language" onchange="changeLanguage(this.value)">
<option value="en" <?php echo $lang === 'en' ? 'selected' : ''; ?>>🇬🇧 English</option>
<option value="fr-ca" <?php echo $lang === 'fr-ca' ? 'selected' : ''; ?>>🇨🇦 Français (CA)</option>
</select>
</form>
<p>Current language: <span class="translation" data-key="current_language"><?php echo htmlspecialchars($lang, ENT_QUOTES, 'UTF-8'); ?></span></p>
<p>Sample translation: <span class="translation" data-key="buttons.submit"><?php echo htmlspecialchars($language->get('buttons.submit', 'Submit'), ENT_QUOTES, 'UTF-8'); ?></span></p>
<p class="error-message" id="errorMessage"></p>
<!-- <div class="language-files">
<h3>Detected Language Files</h3>
<p class="folder-url">Languages folder URL: <?php echo htmlspecialchars($languages_folder_url, ENT_QUOTES, 'UTF-8'); ?></p>
<?php if (empty($language_files)): ?>
<p>No language files found in the languages/ directory.</p>
<?php else: ?>
<ul>
<?php foreach ($language_files as $file): ?>
<li class="<?php echo $file['supported'] ? 'supported' : 'unsupported'; ?>">
<?php echo htmlspecialchars($file['friendly_name'], ENT_QUOTES, 'UTF-8'); ?>
(<?php echo htmlspecialchars($file['filename'], ENT_QUOTES, 'UTF-8'); ?>)
<?php if (!$file['supported']): ?>
- Unsupported
<?php endif; ?>
</li>
<?php endforeach; ?>
</ul>
<?php endif; ?>
</div> -->
<script>
function changeLanguage(lang) {
const errorMessage = document.getElementById('errorMessage');
errorMessage.style.display = 'none';
$.ajax({
url: '/ln/set_language.php',
type: 'POST',
data: { lang: lang },
dataType: 'json',
success: function(response) {
if (response.success) {
$('.translation').each(function() {
const key = $(this).data('key');
$(this).fadeTo(200, 0.5, function() {
$(this).text(key === 'current_language' ? response.lang : response.translations[key] || key).fadeTo(200, 1);
});
});
} else {
errorMessage.textContent = 'Failed to change language: ' + (response.error || 'Unknown error');
errorMessage.style.display = 'block';
}
},
error: function(xhr, status, error) {
errorMessage.textContent = 'Network error. Please try again.';
errorMessage.style.display = 'block';
console.error('AJAX error:', error);
}
});
}
</script>
</body>
</html>
7. insert this code (CSS) for the popup menu in the site Settings > Developer > Code for HTML (all pages)
<script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
<script>
window.jQuery || document.write('<script src="/ln/assets/jquery-3.7.1.js"><\/script>');
</script>
<style>
.language-form { margin: 0px; }
.language-select {
appearance: none;
color: white;
-webkit-appearance: none;
-moz-appearance: none;
padding: 5px 5px 5px 5px;
margin: 0;
font-size: 28px;
border: none;
background-color: transparent;
cursor: pointer;
background-image: url('/ln/assets/dropdown-arrow.svg');
background-repeat: no-repeat;
background-position: right 1px center;
background-size: 15px;
min-width: 56px;
transition: border-color 0.2s;
}
.language-select:focus {
outline: none;
border-color: #3b82f6;
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
}
.error-message {
color: #dc2626;
margin: 10px;
display: none;
}
/* Styles for larger screens (above 768px) */
@media (max-width: 769px) {
.language-select {
padding: 5px;
font-size: 12px; /* Larger font for desktop */
background-size: 10px; /* Larger arrow for desktop */
min-width: 30px; /* Larger width for desktop */
}
.language-select:focus {
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1); /* Stronger shadow for desktop */
}
.error-message {
margin: 0px;
}
}
</style>
8. Next, insert this code into each page php insertion
<?php
echo __DIR__;
include 'ln/language_init.php';
?>
-- [Screenshot 2025-05-23 at 21.37.57|523x387](upload://x2DI1KpZsDqlAJuAFMHbrzJvBFh.png)
9. Add an Embedded Content element where you want your menu
-- [Screenshot 2025-05-23 at 21.40.28|149x75](upload://wmXlMajKWiI3BifXVwuBJbx7yfK.png)
And this code :
<div id="languagePopup"></div>
<script>
// Robust function to get cookie value
function getCookie(name) {
const cookies = document.cookie.split(';');
for (let cookie of cookies) {
cookie = cookie.trim();
if (cookie.startsWith(name + '=')) {
const value = cookie.substring(name.length + 1);
console.log(`Found cookie: ${name}=${value}`);
return value;
}
}
console.log(`Cookie not found: ${name}`);
return null;
}
// Initialize language dropdown
document.addEventListener('DOMContentLoaded', function() {
const container = document.getElementById('languagePopup');
if (!container) {
console.error('Language popup container not found');
return;
}
// Get current language from cookie or default to 'fr-ca'
const currentLang = getCookie('lang') || 'fr-ca';
console.log(`Current language: ${currentLang}`);
// Create form and select elements
const form = document.createElement('form');
form.className = 'language-form';
form.id = 'languageForm';
const select = document.createElement('select');
select.className = 'language-select';
select.name = 'lang';
select.setAttribute('aria-label', 'Select language');
// Define language options
const options = [
{ value: 'en', text: '🇬🇧 en' },
{ value: 'fr-ca', text: '🇨🇦 fr' }
];
// Add options to select
options.forEach(option => {
const opt = document.createElement('option');
opt.value = option.value;
opt.textContent = option.text;
if (option.value === currentLang) {
opt.selected = true;
}
select.appendChild(opt);
});
// Add onchange event
select.onchange = function() {
changeLanguage(this.value);
};
// Create error message paragraph
const errorMessage = document.createElement('p');
errorMessage.className = 'error-message';
errorMessage.id = 'errorMessage';
errorMessage.style.display = 'none';
// Append elements to form and container
form.appendChild(select);
container.appendChild(form);
container.appendChild(errorMessage);
});
// Function to change language via AJAX
function changeLanguage(lang) {
console.log(`Changing language to: ${lang}`);
const errorMessage = document.getElementById('errorMessage');
errorMessage.style.display = 'none';
$.ajax({
url: '/ln/set_language.php',
type: 'POST',
data: { lang: lang },
dataType: 'json',
success: function(response) {
console.log('AJAX response:', response);
if (response.success) {
window.location.reload(); // Reload to apply translations
} else {
errorMessage.textContent = 'Failed to change language: ' + (response.error || 'Unknown error');
errorMessage.style.display = 'block';
}
},
error: function(xhr, status, error) {
console.error('AJAX error:', status, error);
errorMessage.textContent = 'Network error. Please try again.';
errorMessage.style.display = 'block';
}
});
}
</script>
10. and finaly your code snippet of what you want to translate. I recommande you to create the whole website in one language, ajuste everything and place your snippet at the end because in preview mode we can’t see the translation
Code snippet can be insert into text or buttons caption using Insert SmartField…
Name your snippet (only usefull for easy finding) and reuse this name into you php code
<?php echo $language->get('EssayezLe'); ?>
–Screenshot 2025-05-23 at 21.49.42|637x500
it should look like this : (you need to change the text color in the site Settings > Developer > Code for HTML (all pages) CSS code )
–Screenshot 2025-05-23 at 21.54.09|190x120
Debugging Steps
If the lang cookie isn’t retrieved:
Console Logs:
Open F12 > Console and check:
Found cookie: lang=en or Cookie not found: lang.
Current language: <value>.
On switch: Changing language to: <value>, AJAX response: {...}.
If Cookie not found, the cookie isn’t set or inaccessible.
Cookies:
Open F12 > Application > Cookies > https://kanjo.app.
Check for lang cookie:
Value: en or fr-ca.
Path: /.
HttpOnly: false.
Secure: Set for HTTPS.
SameSite: Strict.
If missing, test set_language.php:
bash
curl -X POST -d "lang=en" https://kanjo.app/ln/set_language.php
Should return {"success":true,"lang":"en",...}.
Test Cookie:
Create /var/www/html/ln/test_cookie.php:
php
<?php
setcookie('lang', 'en', [
'expires' => time() + (30 * 24 * 60 * 60),
'path' => '/',
'secure' => isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on',
'httponly' => **false**,
'samesite' => 'Strict'
]);
echo "Cookie set. Check browser cookies.";
?>
Access https://kanjo.app/ln/test_cookie.php and verify cookie.
Translations:
If <?php echo $language->get('TranslationXYZ'); ?> fails, ensure language_init.php is included.
Test with /var/www/html/ln/test.php:
php
<?php
include 'language_init.php';
global $language;
echo $language->get('TranslationXYZ', 'Default text');
?>
Error Log:
Check /var/www/html/ln/error.log:
bash
cat /var/www/html/ln/error.log
File Paths:
Verify:
bash
ls -l /var/www/html/ln/languages/
ls -l /var/www/html/ln/assets/
Expected Output
Console Logs:
Found cookie: lang=en or Cookie not found: lang.
Current language: en or fr-ca.
On switch: Changing language to: en, AJAX response: {success: true, ...}.
Dropdown: Reflects lang cookie (en or fr-ca).
Cookie: lang=en or fr-ca in F12 > Application > Cookies.
Translations:
lang=en: <p>Hello, World!</p>
lang=fr-ca: <p>Bonjour le monde !</p>