Aleksandr Trofimov пре 2 недеља
родитељ
комит
8c8e0e5a45

+ 1 - 1
AGENTS.md

@@ -18,7 +18,7 @@ Pick glyphs from https://lucide.dev; the vendored copies live in `plugins_public
 
 Add a missing English or Russian string with `scripts/add_lang_key.php`. Do not insert keys by hand into language XML, `sys_localization_keys`, or `sys_localization_strings`.
 
-Run it in the php container. Arguments are language (`en` or `ru`), key, and translation. An existing translation is left unchanged. The script appends the key to the right language file (`_sys_…` to system, `_bx_posts_…` to Posts, and so on), inserts it into the database when that language is installed, and recompiles the language cache.
+Run it in the php container. Arguments are language (`en` or `ru`), key, and translation. An existing translation is left unchanged. The script inserts the key in the right language file next to the closest existing keys (`_sys_…` to system, `_bx_posts_…` to Posts, and so on), inserts it into the database when that language is installed, and recompiles the language cache.
 
 ```bash
 docker exec -i una-php-1 php /opt/una/scripts/add_lang_key.php en _sys_example "Hello"

+ 1 - 1
modules/boonex/english/data/langs/system/en.xml

@@ -2571,6 +2571,7 @@
     <string name="_sys_agents_assistants_field_kwg_text"><![CDATA[Text]]></string>
     <string name="_sys_agents_assistants_field_kwg_faq_q"><![CDATA[Question]]></string>
     <string name="_sys_agents_assistants_field_kwg_faq_a"><![CDATA[Answer]]></string>
+    <string name="_sys_agents_assistants_files_txt_size"><![CDATA[Size]]></string>
     <string name="_sys_agents_assistants_popup_add_knowledge"><![CDATA[Add Knowledge]]></string>
     <string name="_sys_agents_assistants_popup_ask"><![CDATA[Ask Assistant]]></string>
     <string name="_sys_agents_assistants_msg_knowledge_added"><![CDATA[Knowledge was added.]]></string>
@@ -4590,5 +4591,4 @@
     <string name="_sys_redirect_confirmation"><![CDATA[<div class="bx-def-font-h2 bx-def-margin-sec-bottom">You're going to a link outside {1}</div><div class="bx-def-font-small">Link: {0}</div><div class="bx-def-margin-sec-top">Are you sure you want to follow this link?</div>]]></string>
     <string name="_sys_redirect_confirmation_harmful"><![CDATA[<div class="bx-def-font-h2 bx-def-margin-sec-bottom">You're going to a potentially <b>harmful</b> link {1}</div><div class="bx-def-font-small">Link: {0}</div><div class="bx-def-margin-sec-top">Are you sure you want to follow this link?</div>]]></string>
     <string name="_sys_leave_page_confirmation"><![CDATA[<div class="bx-def-font-h2">Leave the page?</div><div class="bx-def-margin-sec-top">If you do it, your changes will not be saved.</div>]]></string>
-    <string name="_sys_agents_assistants_files_txt_size"><![CDATA[Size]]></string>
 </resources>

+ 1 - 1
modules/boonex/russian/data/langs/system/ru.xml

@@ -2541,6 +2541,7 @@
         <string name="_sys_agents_assistants_field_kwg_text"><![CDATA[Текст]]></string>
         <string name="_sys_agents_assistants_field_kwg_faq_q"><![CDATA[Вопрос]]></string>
         <string name="_sys_agents_assistants_field_kwg_faq_a"><![CDATA[Ответ]]></string>
+        <string name="_sys_agents_assistants_files_txt_size"><![CDATA[Size]]></string>
         <string name="_sys_agents_assistants_popup_add_knowledge"><![CDATA[Добавить знания]]></string>
         <string name="_sys_agents_assistants_popup_ask"><![CDATA[Ask Assistant]]></string>
         <string name="_sys_agents_assistants_msg_knowledge_added"><![CDATA[Знания были добавлены.]]></string>
@@ -4503,5 +4504,4 @@
   <string name="_sys_redirect_confirmation"><![CDATA[<div class="bx-def-font-h2 bx-def-margin-sec-bottom">Вы переходите по ссылке за пределы сайта "{1}"</div><div class="bx-def-font-small">Ссылка: {0}</div><div class="bx-def-margin-sec-top">Вы действительно хотите перейти по этой ссылке?</div>]]></string>
   <string name="_sys_redirect_confirmation_harmful"><![CDATA[<div class="bx-def-font-h2 bx-def-margin-sec-bottom">Вы переходите по <b>опасной</b> ссылке за пределы сайта "{1}"</div><div class="bx-def-font-small">Ссылка: {0}</div><div class="bx-def-margin-sec-top">Вы действительно хотите перейти по этой ссылке?</div>]]></string>
   <string name="_sys_leave_page_confirmation"><![CDATA[<div class="bx-def-font-h2">Покинуть страницу?</div><div class="bx-def-margin-sec-top">Если Вы это сделаете, ваши изменения не будут сохранены.</div>]]></string>
-    <string name="_sys_agents_assistants_files_txt_size"><![CDATA[Size]]></string>
 </resources>

+ 128 - 2
scripts/add_lang_key.php

@@ -295,11 +295,12 @@ function writeLangString($sFile, $sLang, $sKey, $sTranslation)
             . '</resources>' . $sNl;
     }
 
-    $iPos = strrpos($sXml, '</resources>');
+    $iPos = insertionOffset($sXml, $sKey);
     if ($iPos === false)
         return false;
 
-    $sLine = '    <string name="' . htmlspecialchars($sKey, ENT_QUOTES | ENT_XML1, 'UTF-8') . '"><![CDATA[' . cdata($sTranslation) . ']]></string>' . $sNl;
+    $sIndent = indentAt($sXml, $iPos);
+    $sLine = $sIndent . '<string name="' . htmlspecialchars($sKey, ENT_QUOTES | ENT_XML1, 'UTF-8') . '"><![CDATA[' . cdata($sTranslation) . ']]></string>' . $sNl;
     if ($iPos > 0 && $sXml[$iPos - 1] !== "\n")
         $sLine = $sNl . $sLine;
 
@@ -307,6 +308,131 @@ function writeLangString($sFile, $sLang, $sKey, $sTranslation)
     return file_put_contents($sFile, $sXml, LOCK_EX) !== false;
 }
 
+/**
+ * Byte offset where $sKey should be inserted: after the closest earlier
+ * sibling, or before </resources> when nothing shares a meaningful prefix.
+ * Sibling groups stay intact, so a new key lands beside a family rather
+ * than between two keys that belong together.
+ */
+function insertionOffset($sXml, $sKey)
+{
+    $iFallback = strrpos($sXml, '</resources>');
+    if ($iFallback === false)
+        return false;
+
+    $aEntries = langStringEntries($sXml);
+    if (!$aEntries)
+        return $iFallback;
+
+    $iBest = 0;
+    foreach ($aEntries as $aEntry) {
+        $iScore = commonNonEmptySegments($sKey, $aEntry['name']);
+        if ($iScore > $iBest)
+            $iBest = $iScore;
+    }
+    if ($iBest < 2)
+        return $iFallback;
+
+    $aClose = array();
+    foreach ($aEntries as $i => $aEntry) {
+        if (commonNonEmptySegments($sKey, $aEntry['name']) === $iBest)
+            $aClose[$i] = $aEntry;
+    }
+
+    $iPred = null;
+    $sPredName = null;
+    foreach ($aClose as $i => $aEntry) {
+        if (strcmp($aEntry['name'], $sKey) < 0 && ($sPredName === null || strcmp($aEntry['name'], $sPredName) > 0)) {
+            $iPred = $i;
+            $sPredName = $aEntry['name'];
+        }
+    }
+    if ($iPred !== null)
+        return clusterEdge($aEntries, $iPred, $iBest, false);
+
+    $iSucc = null;
+    $sSuccName = null;
+    foreach ($aClose as $i => $aEntry) {
+        if (strcmp($aEntry['name'], $sKey) > 0 && ($sSuccName === null || strcmp($aEntry['name'], $sSuccName) < 0)) {
+            $iSucc = $i;
+            $sSuccName = $aEntry['name'];
+        }
+    }
+    if ($iSucc !== null)
+        return clusterEdge($aEntries, $iSucc, $iBest, true);
+
+    return $iFallback;
+}
+
+function clusterEdge($aEntries, $iAnchor, $iMinScore, $bStart)
+{
+    $sAnchor = $aEntries[$iAnchor]['name'];
+    $iEdge = $iAnchor;
+
+    if ($bStart) {
+        for ($i = $iAnchor - 1; $i >= 0; $i--) {
+            if (commonNonEmptySegments($sAnchor, $aEntries[$i]['name']) <= $iMinScore)
+                break;
+            $iEdge = $i;
+        }
+        return $aEntries[$iEdge]['start'];
+    }
+
+    $iCount = count($aEntries);
+    for ($i = $iAnchor + 1; $i < $iCount; $i++) {
+        if (commonNonEmptySegments($sAnchor, $aEntries[$i]['name']) <= $iMinScore)
+            break;
+        $iEdge = $i;
+    }
+    return $aEntries[$iEdge]['end'];
+}
+
+function langStringEntries($sXml)
+{
+    $aEntries = array();
+    $sPattern = '/^([ \t]*)<string\b[^>]*\bname=(["\'])(.*?)\2[^>]*>.*?<\/string>[ \t]*(?:\r?\n)?/ms';
+    if (!preg_match_all($sPattern, $sXml, $aMatches, PREG_OFFSET_CAPTURE))
+        return $aEntries;
+
+    foreach ($aMatches[0] as $i => $aFull) {
+        $aEntries[] = array(
+            'name' => html_entity_decode($aMatches[3][$i][0], ENT_QUOTES | ENT_XML1, 'UTF-8'),
+            'indent' => $aMatches[1][$i][0],
+            'start' => $aFull[1],
+            'end' => $aFull[1] + strlen($aFull[0]),
+        );
+    }
+    return $aEntries;
+}
+
+function commonNonEmptySegments($sA, $sB)
+{
+    $aA = explode('_', $sA);
+    $aB = explode('_', $sB);
+    $n = min(count($aA), count($aB));
+    $iNonEmpty = 0;
+    for ($i = 0; $i < $n; $i++) {
+        if ($aA[$i] !== $aB[$i])
+            break;
+        if ($aA[$i] !== '')
+            $iNonEmpty++;
+    }
+    return $iNonEmpty;
+}
+
+function indentAt($sXml, $iPos)
+{
+    $aEntries = langStringEntries($sXml);
+    $sIndent = '    ';
+    foreach ($aEntries as $aEntry) {
+        if ($aEntry['start'] <= $iPos)
+            $sIndent = $aEntry['indent'];
+        if ($aEntry['start'] >= $iPos)
+            return $aEntry['indent'] !== '' ? $aEntry['indent'] : $sIndent;
+    }
+    return $sIndent;
+}
+
 function cdata($s)
 {
     return str_replace(']]>', ']]]]><![CDATA[>', $s);