mirror of
https://github.com/Dolibarr/dolibarr.git
synced 2025-12-08 18:48: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(),
|
'summary' => array(),
|
||||||
),
|
),
|
||||||
'UxDolibarrContext' => 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),
|
'url' => dol_buildpath($this->baseUrl.'/experimental/experiments/dolibarr-context/index.php', 1),
|
||||||
'icon' => 'fas fa-flask',
|
'icon' => 'fas fa-flask',
|
||||||
'submenu' => array(),
|
'submenu' => array(),
|
||||||
@@ -267,6 +271,15 @@ class Documentation
|
|||||||
'SetAndUseContextVars' => '#titlesection-contextvars',
|
'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'])) {
|
if ($showsubmenu && !empty($menu['submenu'])) {
|
||||||
foreach ($menu['submenu'] as $key => $item) {
|
foreach ($menu['submenu'] as $key => $item) {
|
||||||
print '<li class="summary-title ">';
|
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>';
|
print '<h3 class="level-'.$level.'">'.$langs->trans($key).'</h3>';
|
||||||
|
}
|
||||||
|
|
||||||
if ($showsubmenu_summary) {
|
if ($showsubmenu_summary) {
|
||||||
$this->displaySummary($item, $level);
|
$this->displaySummary($item, $level);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,14 @@
|
|||||||
// This file is purely for IDE autocompletion and developer convenience.
|
/** This file is purely for IDE autocompletion and developer convenience.
|
||||||
// It is never executed or loaded in Dolibarr itself.
|
* It is never executed or loaded in Dolibarr itself.
|
||||||
|
*
|
||||||
// MOCK DEFINITION: Dolibarr.tools
|
* MOCK DEFINITION: Dolibarr.tools
|
||||||
// This mock helps your code editor understand the structure of Dolibarr.tools
|
* This mock helps your code editor understand the structure of Dolibarr.tools
|
||||||
// and provides autocomplete hints, parameter hints, and inline documentation.
|
* and provides autocomplete hints, parameter hints, and inline documentation.
|
||||||
// You can safely edit this file to add all standard Dolibarr tools for autocompletion.
|
* You can safely edit this file to add all standard Dolibarr tools for autocompletion.
|
||||||
|
*
|
||||||
|
* @SEE dolibarr-context.umd.js
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
var Dolibarr = {
|
var Dolibarr = {
|
||||||
tools: {
|
tools: {
|
||||||
@@ -22,8 +26,146 @@ var Dolibarr = {
|
|||||||
*/
|
*/
|
||||||
setEventMessage: function(msg, type, sticky) {},
|
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.
|
// You can add more standard Dolibarr tools here for IDE autocompletion.
|
||||||
// Example:
|
// Example:
|
||||||
// alertUser: function(msg) {},
|
// 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("Show this help : %cDolibarr.tools.showConsoleHelp();","font-weight: bold;");
|
||||||
console.log(`Documentation for admin only on : %cModule builder ➜ UX Components Doc`,"font-weight: bold;");
|
console.log(`Documentation for admin only on : %cModule builder ➜ UX Components Doc`,"font-weight: bold;");
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// -----------------------------------------------------------------------------
|
|
||||||
// DEBUG MODE
|
// DEBUG MODE
|
||||||
// -----------------------------------------------------------------------------
|
|
||||||
console.groupCollapsed("Dolibarr debug mode");
|
console.groupCollapsed("Dolibarr debug mode");
|
||||||
|
|
||||||
console.log(
|
console.log(
|
||||||
@@ -418,9 +414,7 @@
|
|||||||
console.log("Note : debug mode status is persistent.");
|
console.log("Note : debug mode status is persistent.");
|
||||||
console.groupEnd();
|
console.groupEnd();
|
||||||
|
|
||||||
// -----------------------------------------------------------------------------
|
|
||||||
// HOOKS
|
// HOOKS
|
||||||
// -----------------------------------------------------------------------------
|
|
||||||
console.groupCollapsed("Hooks helpers");
|
console.groupCollapsed("Hooks helpers");
|
||||||
|
|
||||||
console.log(
|
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">
|
<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(); ?>
|
<?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.
|
ExperimentalUxContributionEnd = This structure ensures a clear and modular organization of UX experiments, making testing and future integration more efficient.
|
||||||
|
|
||||||
UxDolibarrContext = JS Dolibarr context
|
UxDolibarrContext = JS Dolibarr context
|
||||||
|
UxDolibarrContextHowItWork = How it's work
|
||||||
|
UxDolibarrContextLangsTool = Langs tool
|
||||||
|
|
||||||
# Start experiements menu title
|
# Start experiements menu title
|
||||||
ExperimentalUxInputAjaxFeedback = Input feedback
|
ExperimentalUxInputAjaxFeedback = Input feedback
|
||||||
@@ -189,3 +191,7 @@ JSDolibarrhooks = JS Dolibarr hooks
|
|||||||
JSDolibarrAwaitHooks = JS Dolibarr Async Hooks (Await Hooks) - sequential execution
|
JSDolibarrAwaitHooks = JS Dolibarr Async Hooks (Await Hooks) - sequential execution
|
||||||
ExampleOfCreatingNewContextTool = Dolibarr Tool : Example of creating a new context tool
|
ExampleOfCreatingNewContextTool = Dolibarr Tool : Example of creating a new context tool
|
||||||
SetEventMessageTool = Dolibarr Tool : Set event message
|
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