Plugin Directory

Changeset 3670528


Ignore:
Timestamp:
08/28/2026 02:32:46 PM (2 months ago)
Author:
aida-martinez
Message:

Version 1.1.0

Location:
netdraw
Files:
30 added
14 edited

Legend:

Unmodified
Added
Removed
  • netdraw/trunk/assets/css/admin.css

    r3620277 r3670528  
    4141    font-style: italic;
    4242    color: #646970;
     43}
     44
     45.netdraw-draw-type-wrap {
     46    display: flex;
     47    align-items: center;
     48    gap: 10px;
     49}
     50
     51.netdraw-qualifying-spots-wrap {
     52    display: flex;
     53    align-items: center;
     54    flex-wrap: wrap;
     55    gap: 10px;
     56}
     57
     58.netdraw-qualifying-spots-wrap .description {
     59    flex-basis: 100%;
     60}
     61
     62.netdraw-hidden {
     63    display: none !important;
     64}
     65
     66/* Qualifying Round column emphasis */
     67.netdraw-admin-round-header.is-qualifying-round {
     68    background: #e7eefb;
     69    border-bottom-color: #2271b1;
     70}
     71
     72.netdraw-admin-round-header.is-qualifying-round h3 {
     73    color: #1d4e89;
     74}
     75
     76.netdraw-admin-match-box.is-qualifying-round {
     77    border-color: #9ec2e6;
     78    background: #ffffff;
     79}
     80
     81.netdraw-admin-match-box.is-qualifying-round:hover {
     82    border-color: #2271b1;
     83}
     84
     85/* Qualifier badge in the admin editor */
     86.netdraw-admin-qualifier-badge {
     87    display: inline-flex;
     88    align-items: center;
     89    justify-content: center;
     90    min-width: 22px;
     91    height: 22px;
     92    padding: 0 6px;
     93    font-size: 11px;
     94    font-weight: 700;
     95    color: #ffffff;
     96    background: #2271b1;
     97    border-radius: 12px;
     98    flex-shrink: 0;
    4399}
    44100
  • netdraw/trunk/assets/css/frontend.css

    r3620277 r3670528  
    3636}
    3737
     38.netdraw-badge.qualifying {
     39    background: #e7eefb;
     40    color: #1d4e89;
     41    border-color: #9ec2e6;
     42}
     43
    3844/* Scroll Wrapper to support wide screen layouts and mobile swiping */
    3945.netdraw-scroll-wrapper {
    … …  
    8187    position: relative;
    8288    padding-right: 40px; /* Gap between rounds */
     89}
     90
     91/* Top level holds one card per top-round match (1 for Standard, N for Qualifying) */
     92.netdraw-top-level {
     93    display: flex;
     94    flex-direction: column;
     95    justify-content: flex-start;
    8396}
    8497
    … …  
    136149.netdraw-player.is-loser {
    137150    color: #94a3b8;
     151}
     152
     153/* Qualified player badge in the qualifying round */
     154.netdraw-qualified-badge {
     155    display: inline-flex;
     156    align-items: center;
     157    justify-content: center;
     158    min-width: 20px;
     159    height: 20px;
     160    padding: 0 6px;
     161    margin-left: 6px;
     162    font-size: 10px;
     163    font-weight: 800;
     164    line-height: 1;
     165    color: #ffffff;
     166    background: #2271b1;
     167    border-radius: 12px;
     168    flex-shrink: 0;
     169}
     170
     171/* Qualifying round cards get a subtle blue tint so the draw truncation is obvious */
     172.netdraw-card.is-qualifying-round {
     173    border-color: #9ec2e6;
     174}
     175
     176.netdraw-card.is-qualifying-round:hover {
     177    border-color: #2271b1;
     178}
     179
     180.netdraw-card.is-qualifying-round.has-winner {
     181    border-color: #15803d;
     182    box-shadow: 0 0 0 1px #15803d;
    138183}
    139184
  • netdraw/trunk/assets/js/admin.js

    r3620296 r3670528  
    11document.addEventListener('DOMContentLoaded', function() {
    22    const sizeSelect = document.getElementById('netdraw_bracket_size');
     3    const drawTypeSelect = document.getElementById('netdraw_draw_type');
     4    const spotsSelect = document.getElementById('netdraw_qualifying_spots');
     5    const spotsWrap = document.querySelector('.netdraw-qualifying-spots-wrap');
    36    const dataInput = document.getElementById('netdraw_bracket_data_input');
    47    const gridContainer = document.getElementById('netdraw_admin_grid_container');
    58
    6     if (!sizeSelect || !dataInput || !gridContainer) {
     9    if (!sizeSelect || !drawTypeSelect || !spotsSelect || !dataInput || !gridContainer) {
    710        return;
    811    }
    … …  
    1720
    1821    // Load initial data
    19     let bracketData = { size: 8, matches: {} };
     22    let bracketData = { size: 8, drawType: 'standard', qualifyingSpots: 4, matches: {} };
    2023    if (typeof netdrawAdminData !== 'undefined' && netdrawAdminData.bracketData) {
    2124        bracketData = netdrawAdminData.bracketData;
    … …  
    2326            bracketData.matches = {};
    2427        }
     28        if (bracketData.drawType !== 'qualifying' && bracketData.drawType !== 'standard') {
     29            bracketData.drawType = 'standard';
     30        }
     31        if (!bracketData.qualifyingSpots) {
     32            bracketData.qualifyingSpots = 4;
     33        }
     34    }
     35
     36    function getTotalRounds() {
     37        return Math.log2(bracketData.size);
     38    }
     39
     40    // Number of rounds actually rendered / propagated.
     41    // Qualifying draws stop at the round where remaining winners == qualifying spots.
     42    function getEffectiveRounds() {
     43        if (bracketData.drawType === 'qualifying' && bracketData.qualifyingSpots >= 2) {
     44            return Math.log2(bracketData.size / bracketData.qualifyingSpots);
     45        }
     46        return getTotalRounds();
     47    }
     48
     49    // Repopulate the qualifying spots dropdown for the current bracket size.
     50    function updateQualifyingSpotsOptions() {
     51        const current = parseInt(bracketData.qualifyingSpots, 10);
     52        spotsSelect.innerHTML = '';
     53        let matched = null;
     54        for (let spot = 2; spot < bracketData.size; spot *= 2) {
     55            const option = document.createElement('option');
     56            option.value = spot;
     57            option.textContent = __('players_n', '%d Players').replace('%d', spot);
     58            spotsSelect.appendChild(option);
     59            if (spot === current) {
     60                matched = spot;
     61            }
     62        }
     63        if (!matched) {
     64            // Falls back to half the bracket so the round count stays integral.
     65            bracketData.qualifyingSpots = bracketData.size / 2;
     66            matched = bracketData.qualifyingSpots;
     67        }
     68        spotsSelect.value = matched.toString();
    2569    }
    2670
    … …  
    2872    function syncBracketData(size) {
    2973        bracketData.size = parseInt(size, 10);
    30         const rounds = Math.log2(bracketData.size);
     74        updateQualifyingSpotsOptions();
     75        const rounds = getTotalRounds();
    3176        const newMatches = {};
    3277
    … …  
    4691    }
    4792
    48     // Propagate winners from round to round
     93    // Propagate winners from round to round (stopping at the qualifying round)
    4994    function propagateWinners() {
    50         const rounds = Math.log2(bracketData.size);
    51        
    52         // Clear all subsequent rounds' player names first, we will rebuild them
    53         for (let r = 2; r <= rounds; r++) {
     95        const effectiveRounds = getEffectiveRounds();
     96
     97        // Clear all displayed subsequent rounds' player names first, we will rebuild them
     98        for (let r = 2; r <= effectiveRounds; r++) {
    5499            const matchCount = bracketData.size / Math.pow(2, r);
    55100            for (let m = 1; m <= matchCount; m++) {
    … …  
    60105        }
    61106
    62         // Forward propagation loop
    63         for (let r = 1; r < rounds; r++) {
     107        // Forward propagation loop (winners never propagate out of the qualifying round)
     108        for (let r = 1; r < effectiveRounds; r++) {
    64109            const matchCount = bracketData.size / Math.pow(2, r);
    65110            for (let m = 1; m <= matchCount; m++) {
    66111                const matchId = `r${r}_m${m}`;
    67112                const match = bracketData.matches[matchId];
    68                
     113
    69114                let winnerName = '';
    70115                if (match.winner === 'p1') {
    … …  
    106151    // Get round display name
    107152    function getRoundName(r, totalRounds) {
    108         if (r === totalRounds) {
     153        if (bracketData.drawType === 'qualifying' && r === getEffectiveRounds()) {
     154            return __('qualifying_round', 'Qualifying Round');
     155        } else if (r === totalRounds) {
    109156            return __('finals', 'Finals');
    110157        } else if (r === totalRounds - 1) {
    … …  
    119166    function renderEditor() {
    120167        gridContainer.innerHTML = '';
    121         const rounds = Math.log2(bracketData.size);
     168        const rounds = getEffectiveRounds();
     169        const totalRounds = getTotalRounds();
    122170
    123171        for (let r = 1; r <= rounds; r++) {
     172            const isQualifyingRound = (bracketData.drawType === 'qualifying' && r === rounds);
    124173            const roundCol = document.createElement('div');
    125174            roundCol.className = 'netdraw-admin-round-col';
    126175           
    127176            const roundHeader = document.createElement('div');
    128             roundHeader.className = 'netdraw-admin-round-header';
    129             roundHeader.innerHTML = `<h3>${getRoundName(r, rounds)}</h3><span class="netdraw-round-info">${__('round_n', 'Round %d').replace('%d', r)}</span>`;
     177            roundHeader.className = 'netdraw-admin-round-header' + (isQualifyingRound ? ' is-qualifying-round' : '');
     178            roundHeader.innerHTML = `<h3>${getRoundName(r, totalRounds)}</h3><span class="netdraw-round-info">${__('round_n', 'Round %d').replace('%d', r)}</span>`;
    130179            roundCol.appendChild(roundHeader);
    131180
    … …  
    139188
    140189                const matchBox = document.createElement('div');
    141                 matchBox.className = `netdraw-admin-match-box ${match.winner ? 'has-winner' : ''}`;
     190                matchBox.className = `netdraw-admin-match-box ${match.winner ? 'has-winner' : ''}` + (isQualifyingRound ? ' is-qualifying-round' : '');
    142191                matchBox.dataset.matchId = matchId;
    143192
    … …  
    168217                p1Row.appendChild(p1Input);
    169218
     219                if (isQualifyingRound && match.winner === 'p1') {
     220                    const qBadge = document.createElement('span');
     221                    qBadge.className = 'netdraw-admin-qualifier-badge';
     222                    qBadge.title = __('qualified', 'Qualified');
     223                    qBadge.textContent = __('qualified_q', 'Q');
     224                    p1Row.appendChild(qBadge);
     225                }
     226
    170227                const p1WinBtn = document.createElement('button');
    171228                p1WinBtn.type = 'button';
    … …  
    198255                }
    199256                p2Row.appendChild(p2Input);
     257
     258                if (isQualifyingRound && match.winner === 'p2') {
     259                    const qBadge = document.createElement('span');
     260                    qBadge.className = 'netdraw-admin-qualifier-badge';
     261                    qBadge.title = __('qualified', 'Qualified');
     262                    qBadge.textContent = __('qualified_q', 'Q');
     263                    p2Row.appendChild(qBadge);
     264                }
    200265
    201266                const p2WinBtn = document.createElement('button');
    … …  
    301366        }
    302367    });
     368
     369    // Handle draw type change
     370    if (drawTypeSelect) {
     371        drawTypeSelect.addEventListener('change', function(e) {
     372            const newType = e.target.value;
     373            if (newType === bracketData.drawType) {
     374                return;
     375            }
     376            if (confirm(__('confirm_type_change', 'Switching the draw type may hide or reveal progression matches depending on the qualifying spots. Do you want to proceed?'))) {
     377                bracketData.drawType = newType;
     378                if (spotsWrap) {
     379                    spotsWrap.classList.toggle('netdraw-hidden', newType !== 'qualifying');
     380                }
     381                syncBracketData(bracketData.size);
     382                renderEditor();
     383            } else {
     384                drawTypeSelect.value = bracketData.drawType;
     385            }
     386        });
     387    }
     388
     389    // Handle qualifying spots change
     390    if (spotsSelect) {
     391        spotsSelect.addEventListener('change', function(e) {
     392            const newSpots = parseInt(e.target.value, 10);
     393            if (newSpots === bracketData.qualifyingSpots) {
     394                return;
     395            }
     396            if (confirm(__('confirm_spots_change', 'Changing the qualifying spots will adjust which rounds are shown. Do you want to proceed?'))) {
     397                bracketData.qualifyingSpots = newSpots;
     398                syncBracketData(bracketData.size);
     399                renderEditor();
     400            } else {
     401                spotsSelect.value = bracketData.qualifyingSpots.toString();
     402            }
     403        });
     404    }
    303405
    304406    // PDF/Print Download Action
    … …  
    402504                            const container = document.getElementById('print-area');
    403505                           
     506                            const isQualifying = data.drawType === 'qualifying';
     507                            const badgeText = isQualifying
     508                                ? ${JSON.stringify(__('qualifying_draw', '%d Player Qualifying Draw — %d Spots'))}.replace('%d', data.size).replace('%d', data.qualifyingSpots)
     509                                : ${JSON.stringify(__('knockout_draw', '%d Player Knockout Draw'))}.replace('%d', data.size);
     510
    404511                            // Header
    405512                            const header = document.createElement('div');
    406513                            header.className = 'netdraw-frontend-header';
    407                             header.innerHTML = '<h2>' + ${JSON.stringify(title)} + '</h2><span class="netdraw-badge">' + ${JSON.stringify(__('knockout_draw', '%d Player Knockout Draw'))}.replace('%d', data.size) + '</span>';
     514                            header.innerHTML = '<h2>' + ${JSON.stringify(title)} + '</h2><span class="netdraw-badge' + (isQualifying ? ' qualifying' : '') + '">' + badgeText + '</span>';
    408515                            container.appendChild(header);
    409516
    … …  
    416523                            const size = parseInt(data.size, 10);
    417524                            const totalRounds = Math.log2(size);
    418 
    419                             function buildTreeNode(round, matchNum, totalRounds, matches) {
     525                            const effectiveRounds = isQualifying ? Math.log2(size / data.qualifyingSpots) : totalRounds;
     526
     527                            function buildTreeNode(round, matchNum, effectiveRounds, matches) {
    420528                                const matchId = 'r' + round + '_m' + matchNum;
    421529                                const match = matches[matchId] || { p1: '', p2: '', score: '', winner: '', datetime: '' };
    422530
     531                                const isQualifyingRound = isQualifying && round === effectiveRounds;
     532
    423533                                const node = document.createElement('div');
    424534                                node.className = 'netdraw-node';
    425535
    426536                                const matchCard = document.createElement('div');
    427                                 matchCard.className = 'netdraw-card' + (match.winner ? ' has-winner' : '');
     537                                matchCard.className = 'netdraw-card' + (match.winner ? ' has-winner' : '') + (isQualifyingRound ? ' is-qualifying-round' : '');
    428538
    429539                                const p1Name = match.p1 || '';
    … …  
    433543                                const p1Tbd = !p1Name ? 'is-tbd' : '';
    434544                                const p2Tbd = !p2Name ? 'is-tbd' : '';
    435 
    436                                 const p1Html = '<div class="netdraw-player p1-slot ' + p1Class + ' ' + p1Tbd + '"><span class="netdraw-pname">' + (p1Name || ${JSON.stringify(__('tbd', 'TBD'))}) + '</span></div>';
    437                                 const p2Html = '<div class="netdraw-player p2-slot ' + p2Class + ' ' + p2Tbd + '"><span class="netdraw-pname">' + (p2Name || ${JSON.stringify(__('tbd', 'TBD'))}) + '</span></div>';
     545                                const qBadge = '<span class="netdraw-qualified-badge">' + ${JSON.stringify(__('qualified_q', 'Q'))} + '</span>';
     546                                const p1Q = (isQualifyingRound && match.winner === 'p1') ? qBadge : '';
     547                                const p2Q = (isQualifyingRound && match.winner === 'p2') ? qBadge : '';
     548
     549                                const p1Html = '<div class="netdraw-player p1-slot ' + p1Class + ' ' + p1Tbd + '"><span class="netdraw-pname">' + (p1Name || ${JSON.stringify(__('tbd', 'TBD'))}) + '</span>' + p1Q + '</div>';
     550                                const p2Html = '<div class="netdraw-player p2-slot ' + p2Class + ' ' + p2Tbd + '"><span class="netdraw-pname">' + (p2Name || ${JSON.stringify(__('tbd', 'TBD'))}) + '</span>' + p2Q + '</div>';
    438551                                const scoreHtml = match.score ? '<div class="netdraw-score-row">' + match.score + '</div>' : '';
    439552                                const datetimeHtml = match.datetime ? '<div class="netdraw-datetime-row">' + match.datetime + '</div>' : '';
    … …  
    445558                                    const childrenContainer = document.createElement('div');
    446559                                    childrenContainer.className = 'netdraw-children';
    447                                     childrenContainer.appendChild(buildTreeNode(round - 1, 2 * matchNum - 1, totalRounds, matches));
    448                                     childrenContainer.appendChild(buildTreeNode(round - 1, 2 * matchNum, totalRounds, matches));
     560                                    childrenContainer.appendChild(buildTreeNode(round - 1, 2 * matchNum - 1, effectiveRounds, matches));
     561                                    childrenContainer.appendChild(buildTreeNode(round - 1, 2 * matchNum, effectiveRounds, matches));
    449562                                    node.appendChild(childrenContainer);
    450563                                }
    … …  
    452565                            }
    453566
    454                             bracketTree.appendChild(buildTreeNode(totalRounds, 1, totalRounds, data.matches));
     567                            const topMatchCount = Math.pow(2, totalRounds - effectiveRounds);
     568                            const topLevel = document.createElement('div');
     569                            topLevel.className = 'netdraw-top-level';
     570                            for (let m = 1; m <= topMatchCount; m++) {
     571                                topLevel.appendChild(buildTreeNode(effectiveRounds, m, effectiveRounds, data.matches));
     572                            }
     573                            bracketTree.appendChild(topLevel);
    455574                           
    456575                            // Dynamic zoom based on size to fit 1 page without layout clipping
  • netdraw/trunk/assets/js/frontend.js

    r3620296 r3670528  
    2424        const size = parseInt(bracketData.size, 10);
    2525        const totalRounds = Math.log2(size);
     26        const isQualifying = bracketData.drawType === 'qualifying' && bracketData.qualifyingSpots >= 2;
     27        // Qualifying draws stop at the round where remaining winners equal the qualifying spots.
     28        const effectiveRounds = isQualifying ? Math.log2(size / bracketData.qualifyingSpots) : totalRounds;
    2629
    2730        // Header for Tournament Info
    2831        const header = document.createElement('div');
    2932        header.className = 'netdraw-frontend-header';
     33        let badgeText = __('knockout_draw', '%d Player Knockout Draw').replace('%d', size);
     34        if (isQualifying) {
     35            badgeText = __('qualifying_draw', '%d Player Qualifying Draw — %d Spots')
     36                .replace('%d', size)
     37                .replace('%d', bracketData.qualifyingSpots);
     38        }
    3039        header.innerHTML = `
    3140            <div class="netdraw-bracket-meta">
    32                 <span class="netdraw-badge">${escapeHtml(__('knockout_draw', '%d Player Knockout Draw').replace('%d', size))}</span>
     41                <span class="netdraw-badge${isQualifying ? ' qualifying' : ''}">${escapeHtml(badgeText)}</span>
    3342            </div>
    3443        `;
    … …  
    4251        bracketTree.className = 'netdraw-bracket-tree';
    4352
    44         // Start recursive tree rendering from the Finals (root node)
    45         // Finals is round = totalRounds, matchNum = 1
    46         const rootNode = buildTreeNode(totalRounds, 1, totalRounds, bracketData.matches);
    47         bracketTree.appendChild(rootNode);
     53        // The top level holds every match card of the effective top round.
     54        // Standard draws have a single Finals card; qualifying draws have one
     55        // card per qualifying spot, each with its own feeder subtree below.
     56        const topMatchCount = Math.pow(2, totalRounds - effectiveRounds);
     57        const topLevel = document.createElement('div');
     58        topLevel.className = 'netdraw-top-level';
     59        for (let m = 1; m <= topMatchCount; m++) {
     60            topLevel.appendChild(buildTreeNode(effectiveRounds, m, effectiveRounds, isQualifying, bracketData.matches));
     61        }
     62        bracketTree.appendChild(topLevel);
    4863       
    4964        scrollWrapper.appendChild(bracketTree);
    … …  
    5570
    5671    /**
    57      * Recursively builds tree nodes (Finals down to Round 1)
     72     * Recursively builds tree nodes (top round down to Round 1).
     73     * For qualifying draws, top round is the qualifying round.
    5874     */
    59     function buildTreeNode(round, matchNum, totalRounds, matches) {
     75    function buildTreeNode(round, matchNum, effectiveRounds, isQualifying, matches) {
    6076        const matchId = `r${round}_m${matchNum}`;
    6177        const match = matches[matchId] || { p1: '', p2: '', score: '', winner: '' };
     78        const isQualifyingRound = isQualifying && round === effectiveRounds;
    6279
    6380        const node = document.createElement('div');
    … …  
    6582
    6683        const matchCard = document.createElement('div');
    67         matchCard.className = `netdraw-card ${match.winner ? 'has-winner' : ''}`;
     84        matchCard.className = `netdraw-card ${match.winner ? 'has-winner' : ''} ${isQualifyingRound ? 'is-qualifying-round' : ''}`;
    6885       
    6986        // Clean player names
    … …  
    7794        const p2Tbd = !p2Name ? 'is-tbd' : '';
    7895
     96        const qualifiedBadge = `<span class="netdraw-qualified-badge" title="${escapeHtml(__('qualified', 'Qualified'))}">${escapeHtml(__('qualified_q', 'Q'))}</span>`;
     97
    7998        // Player 1 DOM
    8099        const p1Html = `
    81100            <div class="netdraw-player p1-slot ${p1Class} ${p1Tbd}" data-player-name="${p1Name}">
    82101                <span class="netdraw-pname">${p1Name || escapeHtml(__('tbd', 'TBD'))}</span>
     102                ${isQualifyingRound && match.winner === 'p1' ? qualifiedBadge : ''}
    83103            </div>
    84104        `;
    … …  
    88108            <div class="netdraw-player p2-slot ${p2Class} ${p2Tbd}" data-player-name="${p2Name}">
    89109                <span class="netdraw-pname">${p2Name || escapeHtml(__('tbd', 'TBD'))}</span>
     110                ${isQualifyingRound && match.winner === 'p2' ? qualifiedBadge : ''}
    90111            </div>
    91112        `;
    … …  
    119140
    120141            // Top feeder: previous round, match odd (2 * m - 1)
    121             const topChild = buildTreeNode(round - 1, 2 * matchNum - 1, totalRounds, matches);
     142            const topChild = buildTreeNode(round - 1, 2 * matchNum - 1, effectiveRounds, isQualifying, matches);
    122143            // Bottom feeder: previous round, match even (2 * m)
    123             const bottomChild = buildTreeNode(round - 1, 2 * matchNum, totalRounds, matches);
     144            const bottomChild = buildTreeNode(round - 1, 2 * matchNum, effectiveRounds, isQualifying, matches);
    124145
    125146            childrenContainer.appendChild(topChild);
    … …  
    180201        const containers = document.querySelectorAll('.netdraw-bracket-container');
    181202        containers.forEach(container => {
    182             const rawData = container.getAttribute('data-bracket');
    183             if (rawData) {
    184                 try {
    185                     const bracketData = JSON.parse(rawData);
    186                     window.netdrawRenderInstance(container, bracketData);
    187                 } catch (e) {
    188                     console.error("NetDraw parsing error:", e);
    189                     container.innerHTML = '<div class="netdraw-error">' + escapeHtml(__('error_parsing', 'Error parsing bracket data.')) + '</div>';
    190                 }
     203            // Each container carries its own bracket data in the data-bracket attribute.
     204            const bracketJson = container.getAttribute('data-bracket');
     205            if (!bracketJson) return;
     206
     207            let bracketData;
     208            try {
     209                bracketData = JSON.parse(bracketJson);
     210            } catch (e) {
     211                console.error("NetDraw parsing error:", e);
     212                container.innerHTML = '<div class="netdraw-error">' + escapeHtml(__('error_parsing', 'Error parsing bracket data.')) + '</div>';
     213                return;
     214            }
     215
     216            try {
     217                window.netdrawRenderInstance(container, bracketData);
     218            } catch (e) {
     219                console.error("NetDraw parsing error:", e);
     220                container.innerHTML = '<div class="netdraw-error">' + escapeHtml(__('error_parsing', 'Error parsing bracket data.')) + '</div>';
    191221            }
    192222        });
  • netdraw/trunk/languages/netdraw-es_ES.po

    r3620296 r3670528  
    11msgid ""
    22msgstr ""
    3 "Project-Id-Version: NetDraw 1.0.0\n"
     3"Project-Id-Version: NetDraw 1.1.0\n"
    44"Report-Msgid-Bugs-To: \n"
    55"POT-Creation-Date: 2026-07-23 18:41+0200\n"
    … …  
    158158msgid "Error parsing bracket data."
    159159msgstr "Error al analizar los datos del cuadro."
     160
     161msgid "Draw Type:"
     162msgstr "Tipo de cuadro:"
     163
     164msgid "Standard"
     165msgstr "Estándar"
     166
     167msgid "Qualifying"
     168msgstr "Clasificatorio"
     169
     170msgid "Main Draw Qualifying Spots:"
     171msgstr "Plazas de clasificación al cuadro principal:"
     172
     173msgid "Qualifying Round"
     174msgstr "Ronda de clasificación"
     175
     176msgid "Qualified"
     177msgstr "Clasificado"
     178
     179msgid "Q"
     180msgstr "Q"
     181
     182msgid "%d Player Qualifying Draw — %d Spots"
     183msgstr "Cuadro clasificatorio de %d jugadores — %d plazas"
     184
     185msgid "Switching the draw type may hide or reveal progression matches depending on the qualifying spots. Do you want to proceed?"
     186msgstr "Cambiar el tipo de cuadro puede ocultar o mostrar partidos de progresión según las plazas de clasificación. ¿Desea continuar?"
     187
     188msgid "Changing the qualifying spots will adjust which rounds are shown. Do you want to proceed?"
     189msgstr "Cambiar las plazas de clasificación ajustará las rondas que se muestran. ¿Desea continuar?"
     190
     191msgid "%d Players"
     192msgstr "%d Jugadores"
     193
     194msgid "How many players advance to the main draw."
     195msgstr "Cuántos jugadores avanzan al cuadro principal."
     196
     197msgid "%d Player"
     198msgid_plural "%d Players"
     199msgstr[0] "%d Jugador"
     200msgstr[1] "%d Jugadores"
  • netdraw/trunk/languages/netdraw-fr_FR.po

    r3620296 r3670528  
    11msgid ""
    22msgstr ""
    3 "Project-Id-Version: NetDraw 1.0.0\n"
     3"Project-Id-Version: NetDraw 1.1.0\n"
    44"Report-Msgid-Bugs-To: \n"
    55"POT-Creation-Date: 2026-07-23 18:41+0200\n"
    … …  
    158158msgid "Error parsing bracket data."
    159159msgstr "Erreur lors de l'analyse des données du tableau."
     160
     161msgid "Draw Type:"
     162msgstr "Type de tableau :"
     163
     164msgid "Standard"
     165msgstr "Standard"
     166
     167msgid "Qualifying"
     168msgstr "Qualifications"
     169
     170msgid "Main Draw Qualifying Spots:"
     171msgstr "Places qualificatives pour le tableau principal :"
     172
     173msgid "Qualifying Round"
     174msgstr "Tour de qualification"
     175
     176msgid "Qualified"
     177msgstr "Qualifié"
     178
     179msgid "Q"
     180msgstr "Q"
     181
     182msgid "%d Player Qualifying Draw — %d Spots"
     183msgstr "Tableau de qualification de %d joueurs — %d places"
     184
     185msgid "Switching the draw type may hide or reveal progression matches depending on the qualifying spots. Do you want to proceed?"
     186msgstr "Changer le type de tableau peut masquer ou révéler les matchs de progression selon les places qualificatives. Voulez-vous continuer ?"
     187
     188msgid "Changing the qualifying spots will adjust which rounds are shown. Do you want to proceed?"
     189msgstr "Modifier les places qualificatives ajustera les tours affichés. Voulez-vous continuer ?"
     190
     191msgid "%d Players"
     192msgstr "%d Joueurs"
     193
     194msgid "How many players advance to the main draw."
     195msgstr "Combien de joueurs se qualifient pour le tableau principal ?"
     196
     197msgid "%d Player"
     198msgid_plural "%d Players"
     199msgstr[0] "%d Joueur"
     200msgstr[1] "%d Joueurs"
  • netdraw/trunk/languages/netdraw-gl_ES.po

    r3620296 r3670528  
    11msgid ""
    22msgstr ""
    3 "Project-Id-Version: NetDraw 1.0.0\n"
     3"Project-Id-Version: NetDraw 1.1.0\n"
    44"Report-Msgid-Bugs-To: \n"
    55"POT-Creation-Date: 2026-07-23 18:45+0200\n"
    … …  
    158158msgid "Error parsing bracket data."
    159159msgstr "Erro ao analizar os datos do cadro."
     160
     161msgid "Draw Type:"
     162msgstr "Tipo de cadro:"
     163
     164msgid "Standard"
     165msgstr "Estándar"
     166
     167msgid "Qualifying"
     168msgstr "Clasificatorio"
     169
     170msgid "Main Draw Qualifying Spots:"
     171msgstr "Prazas de clasificación ao cadro principal:"
     172
     173msgid "Qualifying Round"
     174msgstr "Rolda de clasificación"
     175
     176msgid "Qualified"
     177msgstr "Clasificado"
     178
     179msgid "Q"
     180msgstr "Q"
     181
     182msgid "%d Player Qualifying Draw — %d Spots"
     183msgstr "Cadro clasificatorio de %d xogadores — %d prazas"
     184
     185msgid "Switching the draw type may hide or reveal progression matches depending on the qualifying spots. Do you want to proceed?"
     186msgstr "Cambiar o tipo de cadro pode ocultar ou mostrar partidos de progresión segundo as prazas de clasificación. ¿Quere continuar?"
     187
     188msgid "Changing the qualifying spots will adjust which rounds are shown. Do you want to proceed?"
     189msgstr "Cambiar as prazas de clasificación axustará as roldas que se mostran. ¿Quere continuar?"
     190
     191msgid "%d Players"
     192msgstr "%d Xogadores"
     193
     194msgid "How many players advance to the main draw."
     195msgstr "Cantos xogadores avanzan ao cadro principal."
     196
     197msgid "%d Player"
     198msgid_plural "%d Players"
     199msgstr[0] "%d Xogador"
     200msgstr[1] "%d Xogadores"
  • netdraw/trunk/languages/netdraw-it_IT.po

    r3620296 r3670528  
    11msgid ""
    22msgstr ""
    3 "Project-Id-Version: NetDraw 1.0.0\n"
     3"Project-Id-Version: NetDraw 1.1.0\n"
    44"Report-Msgid-Bugs-To: \n"
    55"POT-Creation-Date: 2026-07-23 18:41+0200\n"
    … …  
    158158msgid "Error parsing bracket data."
    159159msgstr "Errore durante l'analisi dei dati del tabellone."
     160
     161msgid "Draw Type:"
     162msgstr "Tipo di tabellone:"
     163
     164msgid "Standard"
     165msgstr "Standard"
     166
     167msgid "Qualifying"
     168msgstr "Qualificazioni"
     169
     170msgid "Main Draw Qualifying Spots:"
     171msgstr "Posti di qualificazione per il tabellone principale:"
     172
     173msgid "Qualifying Round"
     174msgstr "Turno di qualificazione"
     175
     176msgid "Qualified"
     177msgstr "Qualificato"
     178
     179msgid "Q"
     180msgstr "Q"
     181
     182msgid "%d Player Qualifying Draw — %d Spots"
     183msgstr "Tabellone di qualificazione a %d giocatori — %d posti"
     184
     185msgid "Switching the draw type may hide or reveal progression matches depending on the qualifying spots. Do you want to proceed?"
     186msgstr "Cambiare il tipo di tabellone può nascondere o mostrare le partite di progressione in base ai posti di qualificazione. Vuoi procedere?"
     187
     188msgid "Changing the qualifying spots will adjust which rounds are shown. Do you want to proceed?"
     189msgstr "Modificare i posti di qualificazione regolerà i turni mostrati. Vuoi procedere?"
     190
     191msgid "%d Players"
     192msgstr "%d Giocatori"
     193
     194msgid "How many players advance to the main draw."
     195msgstr "Quanti giocatori avanzano al tabellone principale."
     196
     197msgid "%d Player"
     198msgid_plural "%d Players"
     199msgstr[0] "%d Giocatore"
     200msgstr[1] "%d Giocatori"
  • netdraw/trunk/netdraw.php

    r3620296 r3670528  
    33 * Plugin Name: NetDraw
    44 * Description: A lightweight, spreadsheet-style WordPress plugin for managing and displaying knockout tennis tournaments with visual brackets.
    5  * Version: 1.0.1
     5 * Version: 1.1.0
    66 * Author: Aida Martinez
    77 * Text Domain: netdraw
    … …  
    2020// Constants
    2121// ---------------------------------------------------------------------------
    22 define( 'NETDRAW_VERSION', '1.0.1' );
     22define( 'NETDRAW_VERSION', '1.1.0' );
    2323define( 'NETDRAW_FILE', __FILE__ );
    2424define( 'NETDRAW_DIR', plugin_dir_path( __FILE__ ) );
    … …  
    115115                $bracket_data = wp_json_encode(
    116116                        array(
    117                                 'size'    => 8,
    118                                 'matches' => new stdClass(),
    119                         )
     117                                'size'             => 8,
     118                                'drawType'         => 'standard',
     119                                'qualifyingSpots'  => 4,
     120                                'matches'          => new stdClass(),
     121                        ),
     122                        JSON_UNESCAPED_UNICODE | JSON_HEX_TAG
    120123                );
    121124        }
    … …  
    128131                        'frontendCssUrl' => NETDRAW_URL . 'assets/css/frontend.css',
    129132                        'strings'        => array(
     133                                'draw_type'           => __( 'Draw Type:', 'netdraw' ),
     134                                'standard'            => __( 'Standard', 'netdraw' ),
     135                                'qualifying'          => __( 'Qualifying', 'netdraw' ),
     136                                'qualifying_spots'    => __( 'Main Draw Qualifying Spots:', 'netdraw' ),
     137                                'qualifying_round'    => __( 'Qualifying Round', 'netdraw' ),
     138                                'qualified'           => __( 'Qualified', 'netdraw' ),
     139                                'qualified_q'         => __( 'Q', 'netdraw' ),
     140                                'qualifying_draw'     => __( '%d Player Qualifying Draw — %d Spots', 'netdraw' ),
     141                                'confirm_type_change' => __( 'Switching the draw type may hide or reveal progression matches depending on the qualifying spots. Do you want to proceed?', 'netdraw' ),
     142                                'confirm_spots_change'=> __( 'Changing the qualifying spots will adjust which rounds are shown. Do you want to proceed?', 'netdraw' ),
    130143                                'finals'              => __( 'Finals', 'netdraw' ),
    131144                                'semifinals'          => __( 'Semifinals', 'netdraw' ),
    … …  
    134147                                'match_n'             => __( 'Match %d', 'netdraw' ),
    135148                                'player_n'            => __( 'Player %d', 'netdraw' ),
     149                                'players_n'           => __( '%d Players', 'netdraw' ),
    136150                                'tbd'                 => __( 'TBD', 'netdraw' ),
    137151                                'cannot_set_winner'   => __( 'Cannot set winner for an empty player slot.', 'netdraw' ),
    … …  
    183197        $bracket_data = json_decode( $bracket_data_json, true );
    184198        $size = isset( $bracket_data['size'] ) ? intval( $bracket_data['size'] ) : 8;
     199        $draw_type = isset( $bracket_data['drawType'] ) && 'qualifying' === $bracket_data['drawType'] ? 'qualifying' : 'standard';
     200        $qualifying_spots = isset( $bracket_data['qualifyingSpots'] ) ? intval( $bracket_data['qualifyingSpots'] ) : 4;
     201        if ( $qualifying_spots < 2 || ( $qualifying_spots & ( $qualifying_spots - 1 ) ) !== 0 || $qualifying_spots >= $size ) {
     202                $qualifying_spots = 4;
     203        }
    185204        ?>
    186205        <div class="netdraw-admin-wrapper">
    … …  
    197216                        </div>
    198217
     218                        <div class="netdraw-draw-type-wrap">
     219                                <label for="netdraw_draw_type"><strong><?php esc_html_e( 'Draw Type:', 'netdraw' ); ?></strong></label>
     220                                <select id="netdraw_draw_type" name="netdraw_draw_type" class="postbox" style="margin: 0;">
     221                                        <option value="standard" <?php selected( $draw_type, 'standard' ); ?>><?php esc_html_e( 'Standard', 'netdraw' ); ?></option>
     222                                        <option value="qualifying" <?php selected( $draw_type, 'qualifying' ); ?>><?php esc_html_e( 'Qualifying', 'netdraw' ); ?></option>
     223                                </select>
     224                        </div>
     225
     226                        <div class="netdraw-qualifying-spots-wrap<?php echo 'standard' === $draw_type ? ' netdraw-hidden' : ''; ?>">
     227                                <label for="netdraw_qualifying_spots"><strong><?php esc_html_e( 'Main Draw Qualifying Spots:', 'netdraw' ); ?></strong></label>
     228                                <select id="netdraw_qualifying_spots" name="netdraw_qualifying_spots" class="postbox" style="margin: 0;">
     229                                        <?php for ( $spot = 2; $spot < $size; $spot *= 2 ) : ?>
     230                                                <option value="<?php echo esc_attr( $spot ); ?>" <?php selected( $qualifying_spots, $spot ); ?>><?php echo esc_html( sprintf( _n( '%d Player', '%d Players', $spot, 'netdraw' ), $spot ) ); ?></option>
     231                                        <?php endfor; ?>
     232                                </select>
     233                                <p class="description" style="margin: 0;"><?php esc_html_e( 'How many players advance to the main draw.', 'netdraw' ); ?></p>
     234                        </div>
     235
    199236                        <div class="netdraw-shortcode-display" style="display: flex; align-items: center; gap: 8px;">
    200237                                <label><strong><?php esc_html_e( 'Shortcode:', 'netdraw' ); ?></strong></label>
    … …  
    245282
    246283        // Sanitize input: decode JSON first, then sanitize individual fields.
    247         // Do NOT run sanitize_textarea_field on raw JSON – it strips backslashes
    248         // that are required for valid JSON encoding.
     284        // Do NOT run wp_unslash() on raw JSON – stripslashes() removes the \
     285        // from JSON Unicode escapes (\u00eda), corrupting accented characters.
     286        // WordPress 6.0+ no longer calls wp_magic_quotes(), so $_POST is raw.
    249287        if ( isset( $_POST['netdraw_bracket_data'] ) ) {
    250                 // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- sanitized field-by-field after decoding below.
    251                 $raw_json     = wp_unslash( $_POST['netdraw_bracket_data'] );
     288                // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Decoded via json_decode(); individual fields sanitized below.
     289                $raw_json     = $_POST['netdraw_bracket_data'];
    252290                $data_decoded = json_decode( $raw_json, true );
     291
     292                // If decode failed, WordPress may have added slashes (wp_magic_quotes).
     293                // Try again after removing them.
     294                if ( ! is_array( $data_decoded ) ) {
     295                        $raw_json     = wp_unslash( $raw_json );
     296                        $data_decoded = json_decode( $raw_json, true );
     297                }
    253298
    254299                if ( is_array( $data_decoded ) ) {
    255300                        $sanitized_matches = array();
    256301                        $size              = isset( $data_decoded['size'] ) ? intval( $data_decoded['size'] ) : 8;
     302                        $draw_type         = isset( $data_decoded['drawType'] ) && 'qualifying' === $data_decoded['drawType'] ? 'qualifying' : 'standard';
     303                        $qualifying_spots  = isset( $data_decoded['qualifyingSpots'] ) ? intval( $data_decoded['qualifyingSpots'] ) : 4;
     304
     305                        // Qualifying spots must be a power of two and smaller than the bracket size.
     306                        if ( $qualifying_spots < 2 || ( $qualifying_spots & ( $qualifying_spots - 1 ) ) !== 0 || $qualifying_spots >= $size ) {
     307                                $qualifying_spots = 4;
     308                        }
    257309
    258310                        if ( isset( $data_decoded['matches'] ) && is_array( $data_decoded['matches'] ) ) {
    … …  
    270322
    271323                        $sanitized_payload = array(
    272                                 'size'    => $size,
    273                                 'matches' => $sanitized_matches,
     324                                'size'            => $size,
     325                                'drawType'        => $draw_type,
     326                                'qualifyingSpots' => $qualifying_spots,
     327                                'matches'         => $sanitized_matches,
    274328                        );
    275329
    276330                        // update_post_meta handles slashing internally; do not wrap with wp_slash().
    277                         update_post_meta( $post_id, '_netdraw_bracket_data', wp_json_encode( $sanitized_payload ) );
     331                        update_post_meta( $post_id, '_netdraw_bracket_data', wp_json_encode( $sanitized_payload, JSON_UNESCAPED_UNICODE | JSON_HEX_TAG ) );
    278332                }
    279333        }
    … …  
    303357        wp_enqueue_script( 'netdraw-frontend-script' );
    304358
    305         wp_localize_script(
    306                 'netdraw-frontend-script',
    307                 'netdrawFrontData',
    308                 array(
    309                         'strings' => array(
    310                                 'tbd'            => __( 'TBD', 'netdraw' ),
    311                                 'knockout_draw'  => __( '%d Player Knockout Draw', 'netdraw' ),
    312                                 'no_data'        => __( 'No bracket data available.', 'netdraw' ),
    313                                 'match_score'    => __( 'Match Score', 'netdraw' ),
    314                                 'match_datetime' => __( 'Match Date & Time', 'netdraw' ),
    315                                 'error_parsing'  => __( 'Error parsing bracket data.', 'netdraw' ),
    316                         ),
    317                 )
    318         );
    319 
    320         // Output HTML container with the JSON data directly in data attribute (no inline scripts)
     359        // Localize the shared strings only once per page. Per-instance bracket
     360        // data is passed inline below via the container's data-bracket attribute,
     361        // so multiple shortcodes on the same page each render their own bracket.
     362        static $strings_localized = false;
     363        if ( ! $strings_localized ) {
     364                $strings_localized = true;
     365                wp_localize_script(
     366                        'netdraw-frontend-script',
     367                        'netdrawFrontData',
     368                        array(
     369                                'strings' => array(
     370                                        'tbd'             => __( 'TBD', 'netdraw' ),
     371                                        'knockout_draw'   => __( '%d Player Knockout Draw', 'netdraw' ),
     372                                        'qualifying_draw' => __( '%d Player Qualifying Draw — %d Spots', 'netdraw' ),
     373                                        'qualifying_round'=> __( 'Qualifying Round', 'netdraw' ),
     374                                        'qualified'       => __( 'Qualified', 'netdraw' ),
     375                                        'qualified_q'     => __( 'Q', 'netdraw' ),
     376                                        'no_data'         => __( 'No bracket data available.', 'netdraw' ),
     377                                        'match_score'     => __( 'Match Score', 'netdraw' ),
     378                                        'match_datetime'  => __( 'Match Date & Time', 'netdraw' ),
     379                                        'error_parsing'   => __( 'Error parsing bracket data.', 'netdraw' ),
     380                                ),
     381                        )
     382                );
     383        }
     384
     385        // Output HTML container (data is passed inline, not via wp_localize_script)
    321386        ob_start();
    322387        ?>
  • netdraw/trunk/readme.txt

    r3620296 r3670528  
    55Tested up to: 7.0
    66Requires PHP: 7.4
    7 Stable tag: 1.0.1
     7Stable tag: 1.1.0
    88License: GPL-2.0-or-later
    99License URI: https://www.gnu.org/licenses/gpl-2.0.html
    … …  
    2121* **Simple Setup**: Create a new tournament draw just like writing a standard post or page in WordPress.
    2222* **Flexible Sizing**: Choose your tournament size (8, 16, 32, or 64 players) via a simple dropdown selection.
     23* **Draw Types**: Choose between a **Standard** single-elimination draw and a **Qualifying** draw that stops once the configured number of players secure advancement spots into a main draw.
    2324* **Easy Data Entry**: Dynamically generates a clean grid of input fields where you can type in player names for the first round and log scores as matches finish.
    2425* **Automatic Progression**: Mark a player as the winner of a match and the plugin automatically pushes their name forward into the next round's slot — saving you from manual re-typing.
    … …  
    2728
    2829* **Visual Brackets**: Generates a clean, classic, tree-style tournament bracket on any page or post using a simple shortcode (e.g., `[netdraw id="123"]`).
     30* **Qualified (Q) Badges**: In qualifying draws, players who win their final qualifying match get a clear **Q** badge indicating they have advanced to the main draw.
    2931* **Path Highlighting**: Hovering over any player highlights their entire journey (matches won, played, and progressed) across all rounds in real-time.
    3032* **Responsive Design**: Uses a custom CSS tree-structure layout that enables smooth horizontal scrolling on mobile, tablet, and desktop screens without breaking the visual integrity.
    … …  
    47493. Enter a title for your tournament (e.g., *Summer Club Open 2026*).
    48504. Under the **Tournament Bracket Editor** meta box, choose the tournament size (8, 16, 32, or 64 players).
    49 5. Fill in the player names for **Round 1**.
    50 6. As matches finish, fill in the score (e.g., `6-4 6-2`) and click the checkmark next to the winning player's name to automatically progress them to the next round.
    51 7. Click **Publish** or **Update** to save.
     515. Choose the **Draw Type**:
     52   * **Standard** — the bracket plays out to a single champion and finalist.
     53   * **Qualifying** — set the **Main Draw Qualifying Spots** (2, 4, 8, ...). The bracket stops at the round where the remaining winners equal that number; those players advance to the main draw.
     546. Fill in the player names for **Round 1**.
     557. As matches finish, fill in the score (e.g., `6-4 6-2`) and click the checkmark next to the winning player's name to automatically progress them to the next round.
     568. Click **Publish** or **Update** to save.
    5257
    5358= Displaying the Bracket =
    … …  
    6368= What tournament formats are supported? =
    6469
    65 NetDraw currently supports single-elimination (knockout) brackets with 8, 16, 32, or 64 players.
     70NetDraw currently supports single-elimination (knockout) brackets with 8, 16, 32, or 64 players. Draws can be **Standard** (crowned through to a champion) or **Qualifying** (stopped once the configured number of players have secured their advancement spots).
    6671
    6772= Can I use this for sports other than tennis? =
    … …  
    97102
    98103    {
    99       "size": 8,
     104      "size": 16,
     105      "drawType": "qualifying",
     106      "qualifyingSpots": 4,
    100107      "matches": {
    101108        "r1_m1": { "p1": "Federer", "p2": "Nadal", "score": "6-4 6-2", "winner": "p1" },
    102109        "r1_m2": { "p1": "Djokovic", "p2": "Murray", "score": "7-5 6-3", "winner": "p1" },
    103         "r2_m1": { "p1": "Federer", "p2": "Djokovic", "score": "", "winner": "" }
     110        "r2_m1": { "p1": "Federer", "p2": "Sinner", "score": "", "winner": "" }
    104111      }
    105112    }
     113
     114* `drawType` is `"standard"` or `"qualifying"`.
     115* `qualifyingSpots` is only meaningful for qualifying draws; it is always a power of two and smaller than the bracket size.
     116* For qualifying draws, the bracket renders only up to `effectiveRounds = log2(size / qualifyingSpots)` rounds. Match data beyond that (if any) is preserved but hidden, so switching back to Standard is non-destructive.
    106117
    107118= Dynamic Progression Logic =
    … …  
    113124* If m is even, the winner becomes Player 2 (p2) of the next match.
    114125* Subsequent rounds are set to read-only in the admin panel to prevent data out-of-sync issues.
     126* In a qualifying draw, propagation stops at the qualifying round: winners there receive a **Qualified (Q)** badge instead of being pushed into another round.
    115127
    116128== Changelog ==
     129
     130= 1.1.0 =
     131* Added **Qualifying Draw** support: choose Standard or Qualifying draws with configurable Main Draw Qualifying Spots (2, 4, 8, ...).
     132* Qualifying brackets truncate automatically at the round where remaining winners equal the qualifying spots.
     133* Added **Qualified (Q)** advancement badges and a dedicated Qualifying Round label in both the admin editor and the frontend bracket.
     134* Preserves hidden bracket data so switching between draw types is non-destructive.
     135* Fixed multiple shortcodes on the same page each rendering their own bracket instead of always showing the last one.
    117136
    118137= 1.0.1 =
    … …  
    129148== Upgrade Notice ==
    130149
     150= 1.1.0 =
     151Added qualifying draw support with configurable main draw qualifying spots and Qualified (Q) badges.
     152
    131153= 1.0.1 =
    132154Added translation support for Spanish, French, Italian, and Galician.
Note: See TracChangeset for help on using the changeset viewer.