mirror of
https://github.com/Dolibarr/dolibarr.git
synced 2025-12-09 19:18:22 +01:00
UIUX : Experiment Dolibarr JS context and tools - Add tool for langs (#36389)
* Add more doc and simplify hook systeme uasge * doc * doc * lang tool * lang tool * lang tool
This commit is contained in:
@@ -253,6 +253,10 @@ class Documentation
|
||||
'summary' => array(),
|
||||
),
|
||||
'UxDolibarrContext' => array(
|
||||
'url' => dol_buildpath($this->baseUrl.'/experimental/experiments/dolibarr-context/index.php', 1),
|
||||
'icon' => 'fas fa-flask',
|
||||
'submenu' => array(
|
||||
'UxDolibarrContextHowItWork' => array(
|
||||
'url' => dol_buildpath($this->baseUrl.'/experimental/experiments/dolibarr-context/index.php', 1),
|
||||
'icon' => 'fas fa-flask',
|
||||
'submenu' => array(),
|
||||
@@ -267,6 +271,15 @@ class Documentation
|
||||
'SetAndUseContextVars' => '#titlesection-contextvars',
|
||||
),
|
||||
),
|
||||
'UxDolibarrContextLangsTool' => array(
|
||||
'url' => dol_buildpath($this->baseUrl.'/experimental/experiments/dolibarr-context/langs-tool.php', 1),
|
||||
'icon' => 'fas fa-flask',
|
||||
'submenu' => array(),
|
||||
'summary' => array(),
|
||||
),
|
||||
),
|
||||
'summary' => array(),
|
||||
),
|
||||
)
|
||||
);
|
||||
|
||||
@@ -485,7 +498,13 @@ class Documentation
|
||||
if ($showsubmenu && !empty($menu['submenu'])) {
|
||||
foreach ($menu['submenu'] as $key => $item) {
|
||||
print '<li class="summary-title ">';
|
||||
|
||||
if (!empty($item['url'])) {
|
||||
print '<h3 class="level-'.$level.'"><a href="'.dolBuildUrl($item['url']).'" >'.$langs->trans($key).'</a></h3>';
|
||||
} else {
|
||||
print '<h3 class="level-'.$level.'">'.$langs->trans($key).'</h3>';
|
||||
}
|
||||
|
||||
if ($showsubmenu_summary) {
|
||||
$this->displaySummary($item, $level);
|
||||
}
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
// This file is purely for IDE autocompletion and developer convenience.
|
||||
// It is never executed or loaded in Dolibarr itself.
|
||||
|
||||
// MOCK DEFINITION: Dolibarr.tools
|
||||
// This mock helps your code editor understand the structure of Dolibarr.tools
|
||||
// and provides autocomplete hints, parameter hints, and inline documentation.
|
||||
// You can safely edit this file to add all standard Dolibarr tools for autocompletion.
|
||||
/** This file is purely for IDE autocompletion and developer convenience.
|
||||
* It is never executed or loaded in Dolibarr itself.
|
||||
*
|
||||
* MOCK DEFINITION: Dolibarr.tools
|
||||
* This mock helps your code editor understand the structure of Dolibarr.tools
|
||||
* and provides autocomplete hints, parameter hints, and inline documentation.
|
||||
* You can safely edit this file to add all standard Dolibarr tools for autocompletion.
|
||||
*
|
||||
* @SEE dolibarr-context.umd.js
|
||||
*
|
||||
*/
|
||||
|
||||
var Dolibarr = {
|
||||
tools: {
|
||||
@@ -22,8 +26,146 @@ var Dolibarr = {
|
||||
*/
|
||||
setEventMessage: function(msg, type, sticky) {},
|
||||
|
||||
/**
|
||||
* TThe langs tool
|
||||
*/
|
||||
langs: {
|
||||
/**
|
||||
* Load a single locale from cache or fetch
|
||||
* @param {string} domain
|
||||
* @param {string} locale
|
||||
* @returns {Promise<Object>} translation object
|
||||
*/
|
||||
loadLocale(domain, locale) {},
|
||||
|
||||
/**
|
||||
* Load translations for a domain (multiple locales)
|
||||
* @param {string} domain
|
||||
* @param {string} locales - comma-separated list
|
||||
* @returns {Promise<Object>}
|
||||
*/
|
||||
load(domain, locales = currentLocale) {},
|
||||
|
||||
/**
|
||||
* Set the current locale to use for translations
|
||||
* @param {string} locale
|
||||
*/
|
||||
setLocale(locale) {},
|
||||
|
||||
/**
|
||||
* Translate a key using current locale
|
||||
* Supports placeholders like %s, %d, %f (simple sprintf)
|
||||
* @param {string} key
|
||||
* @param {...any} args
|
||||
* @returns {string}
|
||||
*/
|
||||
trans(key, ...args) {},
|
||||
},
|
||||
|
||||
// You can add more standard Dolibarr tools here for IDE autocompletion.
|
||||
// Example:
|
||||
// alertUser: function(msg) {},
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Defines a new secure tool.
|
||||
* @param {string} name Name of the tool
|
||||
* @param {*} value Function, class or object
|
||||
* @param {boolean} overwrite Explicitly allow overwriting an existing tool
|
||||
*
|
||||
* See also dolibarr-context.mock.js for defining all standard Dolibarr tools and creating mock implementations to improve code completion and editor support.
|
||||
*/
|
||||
defineTool(name, value, overwrite = false, triggerHook = true) {},
|
||||
|
||||
/**
|
||||
* Check if tool exists
|
||||
* @param {string} name Tool name
|
||||
* @returns {boolean} true if exists
|
||||
*/
|
||||
checkToolExist(name) {},
|
||||
|
||||
/**
|
||||
* Get read-only snapshot of context variables
|
||||
*/
|
||||
ContextVars() {},
|
||||
|
||||
/**
|
||||
* Defines a new context variable.
|
||||
* @param {string} key
|
||||
* @param {string|number|boolean} value
|
||||
* @param {boolean} overwrite Allow overwriting existing value
|
||||
*/
|
||||
setContextVar(key, value, overwrite = false) {},
|
||||
|
||||
/**
|
||||
* Set multiple context variables
|
||||
* @param {Object} vars Object of key/value pairs
|
||||
* @param {boolean} overwrite Allow overwriting existing values
|
||||
*/
|
||||
setContextVars(vars, overwrite = false) {},
|
||||
|
||||
/**
|
||||
* Get a context variable safely
|
||||
* @param {string} key
|
||||
* @param {*} fallback Optional fallback if variable not set
|
||||
* @returns {*}
|
||||
*/
|
||||
getContextVar(key, fallback = null) {},
|
||||
|
||||
/**
|
||||
* Enable or disable debug mode
|
||||
* @param {boolean} state
|
||||
*/
|
||||
debugMode(state) {},
|
||||
|
||||
/**
|
||||
* Enable or disable debug mode
|
||||
* @returns {int}
|
||||
*/
|
||||
getDebugMode() {},
|
||||
|
||||
/**
|
||||
* Internal logger
|
||||
* Only prints when debug mode is enabled
|
||||
* @param {string} msg
|
||||
*/
|
||||
log(msg) {},
|
||||
|
||||
/**
|
||||
* Executes a hook-like JS event with CustomEvent.
|
||||
* @param {string} hookName Hook identifier
|
||||
* @param {object} data Extra information passed to listeners
|
||||
*/
|
||||
executeHook(hookName, data = {}) {},
|
||||
|
||||
/**
|
||||
* Registers an event listener.
|
||||
* @param {string} eventName Event to listen to
|
||||
* @param {function} callback Listener function
|
||||
*/
|
||||
on(eventName, callback) {},
|
||||
|
||||
/**
|
||||
* Unregister an event listener
|
||||
* @param {string} eventName
|
||||
* @param {function} callback
|
||||
*/
|
||||
off(eventName, callback) {},
|
||||
|
||||
/**
|
||||
* Register an asynchronous hook
|
||||
* @param {string} eventName
|
||||
* @param {function} fn Async function receiving previous result
|
||||
* @param {Object} opts Optional {before, after, id} to control order
|
||||
* @returns {string} The hook ID
|
||||
*/
|
||||
onAwait(eventName, fn, opts = {}) {},
|
||||
|
||||
/**
|
||||
* Execute async hooks sequentially
|
||||
* @param {string} eventName
|
||||
* @param {*} data Input data for first hook
|
||||
* @returns {Promise<*>} Final result after all hooks
|
||||
*/
|
||||
async executeHookAwait(eventName, data) {},
|
||||
};
|
||||
|
||||
@@ -393,11 +393,7 @@
|
||||
console.log("Show this help : %cDolibarr.tools.showConsoleHelp();","font-weight: bold;");
|
||||
console.log(`Documentation for admin only on : %cModule builder ➜ UX Components Doc`,"font-weight: bold;");
|
||||
|
||||
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// DEBUG MODE
|
||||
// -----------------------------------------------------------------------------
|
||||
console.groupCollapsed("Dolibarr debug mode");
|
||||
|
||||
console.log(
|
||||
@@ -418,9 +414,7 @@
|
||||
console.log("Note : debug mode status is persistent.");
|
||||
console.groupEnd();
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// HOOKS
|
||||
// -----------------------------------------------------------------------------
|
||||
console.groupCollapsed("Hooks helpers");
|
||||
|
||||
console.log(
|
||||
|
||||
@@ -0,0 +1,232 @@
|
||||
document.addEventListener('Dolibarr:Init', function(e) {
|
||||
/**
|
||||
* Dolibarr.tools.langs
|
||||
* --------------------
|
||||
* Manage translations in JS context with IndexedDB cache, multi-locale support and fallback.
|
||||
* Parallel loading of language files for performance.
|
||||
* Automatic cache invalidation if Dolibarr version changes.
|
||||
*
|
||||
* Require Dolibarr context vars
|
||||
* DOL_LANG_INTERFACE_URL, MAIN_LANG_DEFAULT, DOL_VERSION
|
||||
*
|
||||
*/
|
||||
const langs = function() {
|
||||
|
||||
const ONE_DAY = 86400000;
|
||||
let currentLocale = Dolibarr.getContextVar('MAIN_LANG_DEFAULT', 'en_US');
|
||||
let translations = {}; // { en_US: {KEY: TEXT}, fr_FR: {...} }
|
||||
let domainsLoaded = {}; // { en_US: Set(['main','other']), fr_FR: Set([...]) }
|
||||
if (!domainsLoaded[currentLocale]) domainsLoaded[currentLocale] = new Set();
|
||||
let domainsRequested = new Set(); // Set of domain names that were requested at least once
|
||||
|
||||
/**
|
||||
* Open or create IndexedDB for caching translations
|
||||
* @returns {Promise<IDBDatabase>}
|
||||
*/
|
||||
async function openDB() {
|
||||
return new Promise((resolve, reject) => {
|
||||
const request = indexedDB.open('DolibarrLangs', 1);
|
||||
request.onupgradeneeded = e => {
|
||||
const db = e.target.result;
|
||||
if (!db.objectStoreNames.contains('langs')) db.createObjectStore('langs');
|
||||
};
|
||||
request.onsuccess = () => resolve(request.result);
|
||||
request.onerror = () => reject(request.error);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get cached translation for a domain + locale
|
||||
* @param {string} domain
|
||||
* @param {string} locale
|
||||
* @returns {Promise<Object|null>}
|
||||
*/
|
||||
async function getCache(domain, locale) {
|
||||
try {
|
||||
const db = await openDB();
|
||||
const tx = db.transaction('langs', 'readonly');
|
||||
const store = tx.objectStore('langs');
|
||||
return new Promise((resolve, reject) => {
|
||||
const request = store.get(`${domain}@${locale}`);
|
||||
request.onsuccess = () => resolve(request.result);
|
||||
request.onerror = () => reject(request.error);
|
||||
});
|
||||
} catch (err) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set cached translation for a domain + locale
|
||||
* @param {string} domain
|
||||
* @param {string} locale
|
||||
* @param {Object} data
|
||||
*/
|
||||
async function setCache(domain, locale, data) {
|
||||
try {
|
||||
const db = await openDB();
|
||||
const tx = db.transaction('langs', 'readwrite');
|
||||
const store = tx.objectStore('langs');
|
||||
const dolibarrVersion = Dolibarr.getContextVar('DOL_VERSION', 0);
|
||||
await store.put({ key: `${domain}@${locale}`, data, timestamp: Date.now(), dolibarrVersion }, `${domain}@${locale}`);
|
||||
} catch (err) {
|
||||
// fail silently
|
||||
Dolibarr.log('Save langs in cache fail');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all cached translations in IndexedDB and in-memory
|
||||
*/
|
||||
async function clearCache(clearMemory = false) {
|
||||
if(clearMemory) {
|
||||
translations = {};
|
||||
domainsLoaded = {};
|
||||
}
|
||||
|
||||
try {
|
||||
const db = await openDB();
|
||||
const tx = db.transaction('langs', 'readwrite');
|
||||
const store = tx.objectStore('langs');
|
||||
await store.clear();
|
||||
Dolibarr.log('Dolibarr.tools.langs: cache cleared');
|
||||
} catch (err) {
|
||||
console.error('Dolibarr.tools.langs: failed to clear cache', err);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load a single locale from cache or fetch
|
||||
* @param {string} domain
|
||||
* @param {string} locale
|
||||
* @returns {Promise<Object>} translation object
|
||||
*/
|
||||
async function loadLocale(domain, locale) {
|
||||
const cache = await getCache(domain, locale);
|
||||
const now = Date.now();
|
||||
const dolibarrVersion = Dolibarr.getContextVar('DOL_VERSION', 0);
|
||||
|
||||
if (cache && cache.data && (now - cache.timestamp < ONE_DAY) && cache.dolibarrVersion === dolibarrVersion) {
|
||||
Dolibarr.log('Langs tool : Load lang from cache');
|
||||
return cache.data;
|
||||
}
|
||||
|
||||
const langInterfaceUrl = Dolibarr.getContextVar('DOL_LANG_INTERFACE_URL', false);
|
||||
if(!langInterfaceUrl) {
|
||||
console.error('Dolibarr langs: missing DOL_LANG_INTERFACE_URL')
|
||||
return;
|
||||
}
|
||||
|
||||
Dolibarr.log('Langs tool : Load lang from interface');
|
||||
const params = new URLSearchParams({ domain, local: locale });
|
||||
const resp = await fetch(`${langInterfaceUrl}?${params.toString()}`);
|
||||
const json = await resp.json();
|
||||
const data = json[locale] || {};
|
||||
await setCache(domain, locale, data);
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load translations for a domain (multiple locales)
|
||||
* @param {string} domain
|
||||
* @param {string} locales - comma-separated list
|
||||
* @returns {Promise<Object>}
|
||||
*/
|
||||
async function load(domain, locales = currentLocale) {
|
||||
const list = locales.split(',');
|
||||
|
||||
// flag domaine as requested for future load when local change
|
||||
domainsRequested.add(domain);
|
||||
|
||||
const results = await Promise.all(list.map(loc => loadLocale(domain, loc)));
|
||||
|
||||
list.forEach((loc, i) => {
|
||||
if (!translations[loc]) translations[loc] = {};
|
||||
Object.assign(translations[loc], results[i]);
|
||||
|
||||
if (!domainsLoaded[loc]) domainsLoaded[loc] = new Set();
|
||||
domainsLoaded[loc].add(domain);
|
||||
});
|
||||
|
||||
return translations;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the current locale to use for translations
|
||||
* @param {string} locale
|
||||
*/
|
||||
async function setLocale(locale, noDomainReload = false) {
|
||||
if (!locale || locale === currentLocale) return;
|
||||
|
||||
const prev = currentLocale;
|
||||
currentLocale = locale;
|
||||
|
||||
if (!domainsLoaded[locale]) domainsLoaded[locale] = new Set();
|
||||
|
||||
if (!noDomainReload) {
|
||||
// priorité : domainsLoaded[prev], sinon fallback sur domainsRequested
|
||||
let toReload = Array.from(domainsLoaded[prev] || []);
|
||||
if (toReload.length === 0) {
|
||||
// aucun domaine marqué comme "loaded" pour prev : utiliser la liste des domaines demandés
|
||||
toReload = Array.from(domainsRequested);
|
||||
}
|
||||
|
||||
for (const domain of toReload) {
|
||||
// load(domain, locale) accepte le param locale ; l'appel charge et met domainsLoaded
|
||||
if (domainsLoaded[locale].size === 0) {
|
||||
await load(domain, locale);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Dolibarr.log(`Locale changed: ${prev} -> ${locale}`);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Translate a key using current locale
|
||||
* Supports placeholders like %s, %d, %f (simple sprintf)
|
||||
* @param {string} key
|
||||
* @param {...any} args
|
||||
* @returns {string}
|
||||
*/
|
||||
function trans(key, ...args) {
|
||||
const text = translations[currentLocale]?.[key] || key;
|
||||
if (!args.length) return text;
|
||||
|
||||
// Utilisation de la fonction sprintf pour le formatage
|
||||
return sprintf(text, ...args);
|
||||
}
|
||||
|
||||
function sprintf(fmt, ...args) {
|
||||
let i = 0;
|
||||
return fmt.replace(/%[%bcdeEfFgGosuxX]/g, (match) => {
|
||||
if (match === '%%') return '%';
|
||||
const arg = args[i++];
|
||||
switch (match) {
|
||||
case '%s': return String(arg);
|
||||
case '%d':
|
||||
case '%u': return Number(arg);
|
||||
case '%f':
|
||||
case '%F': return parseFloat(arg);
|
||||
case '%b': return Number(arg).toString(2);
|
||||
case '%o': return Number(arg).toString(8);
|
||||
case '%x': return Number(arg).toString(16);
|
||||
case '%X': return Number(arg).toString(16).toUpperCase();
|
||||
case '%c': return String.fromCharCode(Number(arg));
|
||||
default: return match;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
load,
|
||||
clearCache,
|
||||
setLocale,
|
||||
trans,
|
||||
get currentLocale() { return currentLocale; }
|
||||
};
|
||||
};
|
||||
|
||||
Dolibarr.defineTool('langs',langs());
|
||||
});
|
||||
@@ -68,7 +68,7 @@ $documentation->showSidebar(); ?>
|
||||
|
||||
<div class="doc-content-wrapper">
|
||||
|
||||
<h1 class="documentation-title"><?php echo $langs->trans($experimentName); ?></h1>
|
||||
<h1 class="documentation-title"><?php echo $langs->trans($experimentName); ?> : <?php echo $langs->trans('UxDolibarrContextHowItWork'); ?></h1>
|
||||
|
||||
<?php $documentation->showSummary(); ?>
|
||||
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
<?php
|
||||
/*
|
||||
* Copyright (C) ---Replace with your own copyright and developer email---
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
//if (! defined('NOREQUIREDB')) define('NOREQUIREDB', '1'); // Do not create database handler $db
|
||||
//if (! defined('NOREQUIREUSER')) define('NOREQUIREUSER', '1'); // Do not load object $user
|
||||
if (! defined('NOREQUIRESOC')) define('NOREQUIRESOC', '1'); // Do not load object $mysoc
|
||||
//if (! defined('NOREQUIRETRAN')) define('NOREQUIRETRAN', '1'); // Do not load object $langs
|
||||
//if (! defined('NOSCANGETFORINJECTION')) define('NOSCANGETFORINJECTION', '1'); // Do not check injection attack on GET parameters
|
||||
//if (! defined('NOSCANPOSTFORINJECTION')) define('NOSCANPOSTFORINJECTION', '1'); // Do not check injection attack on POST parameters
|
||||
//if (! defined('NOTOKENRENEWAL')) define('NOTOKENRENEWAL', '1'); // Do not roll the Anti CSRF token (used if MAIN_SECURITY_CSRF_WITH_TOKEN is on)
|
||||
//if (! defined('NOSTYLECHECK')) define('NOSTYLECHECK', '1'); // Do not check style html tag into posted data
|
||||
if (! defined('NOREQUIREMENU')) define('NOREQUIREMENU', '1'); // If there is no need to load and show top and left menu
|
||||
//if (! defined('NOREQUIREHTML')) define('NOREQUIREHTML', '1'); // If we don't need to load the html.form.class.php
|
||||
//if (! defined('NOREQUIREAJAX')) define('NOREQUIREAJAX', '1'); // Do not load ajax.lib.php library
|
||||
//if (! defined("NOLOGIN")) define("NOLOGIN", '1'); // If this page is public (can be called outside logged session). This include the NOIPCHECK too.
|
||||
//if (! defined('NOIPCHECK')) define('NOIPCHECK', '1'); // Do not check IP defined into conf $dolibarr_main_restrict_ip
|
||||
//if (! defined("MAIN_LANG_DEFAULT")) define('MAIN_LANG_DEFAULT', 'auto'); // Force lang to a particular value
|
||||
//if (! defined("MAIN_AUTHENTICATION_MODE")) define('MAIN_AUTHENTICATION_MODE', 'aloginmodule'); // Force authentication handler
|
||||
//if (! defined('CSRFCHECK_WITH_TOKEN')) define('CSRFCHECK_WITH_TOKEN', '1'); // Force use of CSRF protection with tokens even for GET
|
||||
//if (! defined('NOBROWSERNOTIF')) define('NOBROWSERNOTIF', '1'); // Disable browser notification
|
||||
|
||||
|
||||
// Load Dolibarr environment
|
||||
require '../../../../../../main.inc.php';
|
||||
|
||||
|
||||
/**
|
||||
* @var Conf $conf
|
||||
* @var DoliDB $db
|
||||
* @var HookManager $hookmanager
|
||||
* @var Translate $langs
|
||||
* @var User $user
|
||||
*/
|
||||
|
||||
|
||||
/**
|
||||
* INTERFACE FOR JS CONTEXT LANG
|
||||
*/
|
||||
|
||||
if (empty($dolibarr_nocache)) {
|
||||
$delaycache = '86400';
|
||||
header('Cache-Control: max-age=' . $delaycache . ', public, must-revalidate');
|
||||
header('Pragma: cache'); // This is to avoid to have Pragma: no-cache set by proxy or web server
|
||||
header('Expires: ' . gmdate('D, d M Y H:i:s', time() + (int) $delaycache) . ' GMT'); // This is to avoid to have Expires set by proxy or web server
|
||||
} else {
|
||||
// If any cache on files were disable by config file (for test purpose)
|
||||
header('Cache-Control: no-cache');
|
||||
}
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
|
||||
|
||||
$local = GETPOST('local');
|
||||
if (empty($local)) {
|
||||
$local = $langs->getDefaultLang();
|
||||
}
|
||||
|
||||
$domain = GETPOST('domain');
|
||||
if (empty($domain)) {
|
||||
echo json_encode(['error' => 'Missing domain']);
|
||||
exit;
|
||||
}
|
||||
|
||||
if (!preg_match('/^[A-Za-z0-9_-]+(?:@[A-Za-z0-9_-]+)?$/', $domain)) {
|
||||
echo json_encode(['error' => 'Invalid domain']);
|
||||
exit;
|
||||
}
|
||||
|
||||
|
||||
if (!preg_match('/^[a-z]{1,2}_[A-Z]{1,2}(?:,[a-z]{1,2}_[A-Z]{1,2})*$/', $local)) {
|
||||
echo json_encode(['error' => 'Invalid langs codes']);
|
||||
exit;
|
||||
}
|
||||
|
||||
/*
|
||||
Format for JS:
|
||||
{
|
||||
fr_FR : {KEY:"TEXT", ...},
|
||||
en_US : {KEY:"TEXT", ...}
|
||||
}
|
||||
*/
|
||||
|
||||
$locals = explode(',', $local);
|
||||
$json = new stdClass();
|
||||
|
||||
foreach ($locals as $langCode) {
|
||||
$json->$langCode = [];
|
||||
$outputlangs = new Translate("", $conf);
|
||||
$outputlangs->setDefaultLang($langCode);
|
||||
$outputlangs->load($domain);
|
||||
foreach ($outputlangs->tab_translate as $k => $v) {
|
||||
$json->$langCode[$k] = dolPrintHTML($v); // to escape js and other stuff
|
||||
}
|
||||
}
|
||||
|
||||
print json_encode($json, JSON_PRETTY_PRINT);
|
||||
@@ -0,0 +1,207 @@
|
||||
<?php
|
||||
/*
|
||||
* Copyright (C) 2025 Anthony Damhet <a.damhet@progiseize.fr>
|
||||
* Copyright (C) 2025 Frédéric France <frederic.france@free.fr>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
// Load Dolibarr environment
|
||||
require '../../../../../../main.inc.php';
|
||||
|
||||
/**
|
||||
* @var DoliDB $db
|
||||
* @var HookManager $hookmanager
|
||||
* @var Translate $langs
|
||||
* @var User $user
|
||||
*/
|
||||
|
||||
// Protection if external user
|
||||
if ($user->socid > 0) {
|
||||
accessforbidden();
|
||||
}
|
||||
|
||||
// Includes
|
||||
require_once DOL_DOCUMENT_ROOT . '/admin/tools/ui/class/documentation.class.php';
|
||||
|
||||
// Load documentation translations
|
||||
$langs->load('uxdocumentation');
|
||||
|
||||
//
|
||||
$documentation = new Documentation($db);
|
||||
$group = 'ExperimentalUx';
|
||||
$experimentName = 'UxDolibarrContextLangsTool';
|
||||
|
||||
$experimentAssetsPath = $documentation->baseUrl . '/experimental/experiments/dolibarr-context/assets/';
|
||||
$js = [
|
||||
'/includes/ace/src/ace.js',
|
||||
'/includes/ace/src/ext-statusbar.js',
|
||||
'/includes/ace/src/ext-language_tools.js',
|
||||
$experimentAssetsPath . '/dolibarr-context.umd.js',
|
||||
$experimentAssetsPath . '/dolibarr-tool.langs.js',
|
||||
$experimentAssetsPath . '/dolibarr-tool.seteventmessage.js',
|
||||
];
|
||||
$css = [];
|
||||
|
||||
// Output html head + body - Param is Title
|
||||
$documentation->docHeader($langs->trans($experimentName, $group), $js, $css);
|
||||
|
||||
// Set view for menu and breadcrumb
|
||||
$documentation->view = [$group, 'UxDolibarrContext', $experimentName];
|
||||
|
||||
// Output sidebar
|
||||
$documentation->showSidebar(); ?>
|
||||
<script>
|
||||
Dolibarr.setContextVars(<?php print json_encode([
|
||||
'DOL_VERSION' => DOL_VERSION,
|
||||
'MAIN_LANG_DEFAULT' => $langs->getDefaultLang(),
|
||||
'DOL_LANG_INTERFACE_URL' => dol_buildpath('admin/tools/ui/experimental/experiments/dolibarr-context/langs-tool-interface.php', 1),
|
||||
]) ?>);
|
||||
</script>
|
||||
<div class="doc-wrapper">
|
||||
|
||||
<?php $documentation->showBreadCrumb(); ?>
|
||||
|
||||
<div class="doc-content-wrapper">
|
||||
|
||||
<h1 class="documentation-title"><?php echo $langs->trans($experimentName); ?></h1>
|
||||
|
||||
<?php $documentation->showSummary(); ?>
|
||||
|
||||
<div class="documentation-section">
|
||||
<h2 id="titlesection-basicusage" class="documentation-title">Introduction</h2>
|
||||
|
||||
<p>
|
||||
The Dolibarr Context Langs Tool is a powerful JavaScript utility to manage translations and locales dynamically.<br/>
|
||||
It allows you to load translation domains, set the current language, clear cache, and retrieve translated strings in your scripts.
|
||||
</p>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="documentation-section">
|
||||
<h2 id="titlesection-setup-contextvars" class="documentation-title">Setup Context Variables</h2>
|
||||
<p>
|
||||
Before using the tool, you should declare the necessary context variables on your page.<br/>
|
||||
These variables allow the tool to know the current Dolibarr version, the default language, and the interface URL used to fetch translations.
|
||||
</p>
|
||||
<p>
|
||||
However, like the setEventMessage tool, the Langs tool is a core tool and is always loaded by Dolibarr.<br/>
|
||||
Therefore, in most cases, you do not need to set these variables manually, as they are already defined.
|
||||
</p>
|
||||
<div class="documentation-example">
|
||||
|
||||
<?php
|
||||
$lines = array(
|
||||
'<script nonce="'.getNonce().'" >',
|
||||
'Dolibarr.setContextVars(<?php print json_encode([',
|
||||
' \'DOL_VERSION\' => DOL_VERSION,',
|
||||
' \'MAIN_LANG_DEFAULT\' => $langs->getDefaultLang(),',
|
||||
' \'DOL_LANG_INTERFACE_URL\' => dol_buildpath(\'admin/tools/ui/experimental/experiments/dolibarr-context/langs-tool-interface.php\',1),',
|
||||
']) ?>);',
|
||||
'</script>',
|
||||
);
|
||||
echo $documentation->showCode($lines, 'php');
|
||||
?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="documentation-section">
|
||||
<h2 id="titlesection-basic-usage" class="documentation-title">Basic Usage</h2>
|
||||
<p>
|
||||
The main features of the Langs tool are:
|
||||
</p>
|
||||
<ul>
|
||||
<li>Load translations for a specific domain with caching</li>
|
||||
<li>Set or change the current locale</li>
|
||||
<li>Clear cached translations</li>
|
||||
<li>Retrieve a translated string by key</li>
|
||||
</ul>
|
||||
|
||||
<p>Example:</p>
|
||||
|
||||
<div class="documentation-example">
|
||||
<?php
|
||||
$lines = array(
|
||||
'<script nonce="'.getNonce().'" >',
|
||||
'document.addEventListener(\'Dolibarr:Ready\', async function(e) {',
|
||||
'',
|
||||
' if(Dolibarr.checkToolExist(\'langs\')){ // not mandatory because langs tool will be a core tool',
|
||||
'',
|
||||
' // Load langs',
|
||||
' Dolibarr.tools.langs.load(\'uxdocumentation\'); // will use cache but need to load lang in new local',
|
||||
'',
|
||||
' // Clear cache',
|
||||
' document.getElementById(\'clearCache\').addEventListener(\'click\', async function(e) {',
|
||||
' await Dolibarr.tools.langs.clearCache();',
|
||||
' const txt = Dolibarr.tools.langs.trans(\'CacheCleared\');',
|
||||
' Dolibarr.tools.setEventMessage(txt);',
|
||||
' });',
|
||||
'',
|
||||
' // SET lang in fr_FR',
|
||||
' document.getElementById(\'setlangFr\').addEventListener(\'click\', async function(e) {',
|
||||
' await Dolibarr.tools.langs.setLocale(\'fr_FR\');',
|
||||
' const txt = Dolibarr.tools.langs.trans(\'LangsLocalChangedTo\', \'fr_FR\');',
|
||||
' Dolibarr.tools.setEventMessage(txt);',
|
||||
' });',
|
||||
'',
|
||||
' // SET lang in en_US',
|
||||
' document.getElementById(\'setlangEn\').addEventListener(\'click\', async function(e) {',
|
||||
' await Dolibarr.tools.langs.setLocale(\'en_US\');',
|
||||
' const txt = Dolibarr.tools.langs.trans(\'LangsLocalChangedTo\', \'en_US\');',
|
||||
' Dolibarr.tools.setEventMessage(txt);',
|
||||
' });',
|
||||
'',
|
||||
' // pop a message in current lang',
|
||||
' document.getElementById(\'popmessage\').addEventListener(\'click\', async function(e) {',
|
||||
' const txt = Dolibarr.tools.langs.trans(\'ContextLangToolTest\');',
|
||||
' Dolibarr.tools.setEventMessage(txt);',
|
||||
' });',
|
||||
' }',
|
||||
'});',
|
||||
'</script>',
|
||||
);
|
||||
echo $documentation->showCode($lines, 'php');
|
||||
|
||||
print implode("\n", $lines);
|
||||
?>
|
||||
<p>1. Set the current lang</p>
|
||||
<div >
|
||||
<button id="setlangFr" class="button">Set lang in french</button>
|
||||
<button id="setlangEn" class="button">Set lang in english</button>
|
||||
<button id="clearCache" class="button">Clear cache</button>
|
||||
</div>
|
||||
|
||||
<p>2. Pop translated message</p>
|
||||
<div>
|
||||
<button id="popmessage" class="button">pop message</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<?php
|
||||
// Output close body + html
|
||||
$documentation->docFooter();
|
||||
?>
|
||||
@@ -165,6 +165,8 @@ ExperimentalUxContributionTxt03 = In some cases, variants may be incompatible wi
|
||||
ExperimentalUxContributionEnd = This structure ensures a clear and modular organization of UX experiments, making testing and future integration more efficient.
|
||||
|
||||
UxDolibarrContext = JS Dolibarr context
|
||||
UxDolibarrContextHowItWork = How it's work
|
||||
UxDolibarrContextLangsTool = Langs tool
|
||||
|
||||
# Start experiements menu title
|
||||
ExperimentalUxInputAjaxFeedback = Input feedback
|
||||
@@ -189,3 +191,7 @@ JSDolibarrhooks = JS Dolibarr hooks
|
||||
JSDolibarrAwaitHooks = JS Dolibarr Async Hooks (Await Hooks) - sequential execution
|
||||
ExampleOfCreatingNewContextTool = Dolibarr Tool : Example of creating a new context tool
|
||||
SetEventMessageTool = Dolibarr Tool : Set event message
|
||||
|
||||
ContextLangToolTest = This text is in english
|
||||
LangsLocalChangedTo = Language locale changed to %s. Translations will now use the new locale.
|
||||
CacheCleared = Cache cleared
|
||||
|
||||
4
htdocs/langs/fr_FR/uxdocumentation.lang
Normal file
4
htdocs/langs/fr_FR/uxdocumentation.lang
Normal file
@@ -0,0 +1,4 @@
|
||||
|
||||
ContextLangToolTest = Ce texte est en français
|
||||
LangsLocalChangedTo = La langue a été changée en %s. Les traductions utiliseront désormais cette nouvelle langue.
|
||||
CacheCleared = Cache vidé
|
||||
Reference in New Issue
Block a user