Module:Bangladesh Gazette

Lua

CodeDiscussionEditHistoryLinksLink count Subpages:DocumentationTestsResultsSandboxLive code All modules


This module provides metadata handling and categorization for Bangladesh Gazette files on Wikimedia Commons. It powers the {{Bangladesh Gazette Information}} template.

PurposePurpose

The module generates formatted information boxes for Bangladesh Gazette documents and automatically categorizes them based on gazette type, publication date, part number (for weekly gazettes), and issuing organization (for extraordinary gazettes). It supports both weekly and extraordinary gazette types with bilingual (English/Bengali) display.

UsageUsage

This module is not intended to be invoked directly. Instead, use the {{Bangladesh Gazette Information}} template. See Template:Bangladesh Gazette Information/doc for complete usage instructions.

From template pages:

{{Bangladesh Gazette Information
|type = weekly
|date = 2025-12-04
|part = I
|source = http://www.dpp.gov.bd/...
}}

From Lua modules (if needed):

local p = require('Module:Bangladesh Gazette')
local result = p.infobox(frame)

FunctionsFunctions

Main functionsMain functions

p.infobox(frame)
Main function that generates the complete information box with metadata and categories. This is invoked by {{Bangladesh Gazette Information}}.
Parameters: Receives a frame object with parent arguments from the template
Returns: Formatted wikitext containing the information box and category tags
p.hasInfobox(frame)
Utility function to check if the Bangladesh Gazette Information template is used on the current page.
Returns: "yes" if template is found, "no" otherwise

Internal helper functionsInternal helper functions

The module contains several internal functions that handle specific aspects of data processing:

Part normalization
normalizePart(part) – Accepts Roman numerals (I-VIII), Arabic numerals (1-8), or mixed case, and normalizes to uppercase Roman numerals
Date parsing
parseDate(dateStr) – Extracts year, month, and ISO week number from date strings
getWeekOfMonth(dateStr) – Calculates which week of the month (1-5) a date falls in
getMonthName(monthNum) – Converts month number to English month name
Number conversion
toBengaliNumber(num) – Converts English numerals to Bengali numerals (০-৯)
toEnglishNumber(num) – Converts Bengali numerals to English numerals (0-9)
getOrdinalEn(num) – Generates English ordinals (1st, 2nd, 3rd, etc.)
getOrdinalBn(num) – Generates Bengali ordinals (১ম, ২য়, ৩য়, ৪র্থ, etc.)
Categorization
getWeeklyCategories(args, pageName) – Generates categories for weekly gazettes
getExtraordinaryCategories(args, pageName) – Generates categories for extraordinary gazettes
Source detection
isDPPUrl(url) – Detects if a URL is from the Department of Printing and Publications (dpp.gov.bd)

Data structuresData structures

Part name mappingsPart name mappings

The module maintains mappings for the eight parts of Bangladesh Weekly Gazette:

Part English (Short) Bengali (Short) Category
I Statutory notifications বিধিবদ্ধ প্রজ্ঞাপন Statutory notifications
II Government appointments সরকারি নিয়োগ Government appointments
III Defence notifications প্রতিরক্ষা প্রজ্ঞাপন Defence notifications
IV Patent office notifications পেটেন্ট অফিসের প্রজ্ঞাপন Patent office notifications
V Acts and Bills আইন ও বিল Acts and Bills
VI Supreme Court and Public Service notifications সুপ্রিম কোর্ট ও সরকারি কর্মকমিশনের প্রজ্ঞাপন Supreme Court and Public Service notifications
VII Miscellaneous notifications বিবিধ অবিধিবদ্ধ প্রজ্ঞাপন Miscellaneous notifications
VIII Private advertisements ব্যক্তিগত বিজ্ঞাপন Private advertisements

Long-form descriptions are also maintained for each part in both English and Bengali for use in the information box.

Conversion tablesConversion tables

Categorization schemeCategorization scheme

Weekly gazettesWeekly gazettes

Files with type=weekly are categorized as:

Example: Category:Bangladesh Weekly Gazette from 2025 Private advertisements

Extraordinary gazettesExtraordinary gazettes

Files with type=extraordinary are categorized as:

Example: Category:Bangladesh Extraordinary Gazette from June 2024 by Ministry of Law, Justice and Parliamentary Affairs

Error categorizationError categorization

Files missing required parameters are categorized in:

Field order and auto-generationField order and auto-generation

The information box displays fields in this specific order:

  1. Gazette Type (always first) – Auto-generated bilingual label
  2. Description – Auto-generated from part number for weekly gazettes (overridable)
  3. Part (weekly only) – Normalized and displayed bilingually with full description
  4. Date – Publication date (required)
  5. Volume (weekly only) – ISO week number of year, auto-calculated from date (overridable)
  6. Week (weekly only) – Week of month (1-5), auto-calculated (overridable)
  7. Source – Automatically wrapped with {{Source-DPP}} if from dpp.gov.bd
  8. Organization (extraordinary only) – Ministry or issuing organization
  9. Author – Defaults to Institution:Government of Bangladesh (overridable)
  10. Page Range – Displayed bilingually in both English and Bengali numerals
  11. Permission (near end)
  12. Other Versions (always last)

Bilingual supportBilingual support

The module uses {{LangSwitch}}, {{En}}, and {{Bn}} templates to provide bilingual display:

ExamplesExamples

Weekly gazette with minimal parametersWeekly gazette with minimal parameters

{{#invoke:Bangladesh Gazette|infobox
|type = weekly
|date = 2025-12-04
|part = VIII
|source = http://www.dpp.gov.bd/upload_file/gazettes/59439_93575.pdf
}}

This will:

Extraordinary gazetteExtraordinary gazette

{{#invoke:Bangladesh Gazette|infobox
|type = extraordinary
|description = Notification regarding appointment of High Court judges
|date = 2024-06-10
|ministry = Ministry of Law, Justice and Parliamentary Affairs
|source = https://dpp.gov.bd/site/page/9012
}}

This will categorize in "Bangladesh Extraordinary Gazette from June 2024 by Ministry of Law, Justice and Parliamentary Affairs"

DependenciesDependencies

This module depends on the following templates:

Technical notesTechnical notes

See alsoSee also


Code

-- Module for Bangladesh Gazette information and categorization
local p = {}

local getLabel         = require("Module:Wikidata label")._getLabel
local getSitelinks     = require("Module:Wikidata label")._sitelinks
local getDate          = require("Module:Wikidata date")._date
local authorityControl = require("Module:Authority control")._authorityControl
local core             = require("Module:Core")
local artcore          = require("Module:Artwork/core")
local TagQS            = require('Module:TagQS')

-- Part name mappings for weekly gazettes
-- Short forms for categories
local partNamesShort = {
    ['I'] = 'Statutory notifications',
    ['II'] = 'Government appointments',
    ['III'] = 'Defence notifications',
    ['IV'] = 'Patent office notifications',
    ['V'] = 'Acts and Bills',
    ['VI'] = 'Supreme Court and others',
    ['VII'] = 'Miscellaneous notifications',
    ['VIII'] = 'Private advertisements'
}

-- Bengali short forms for part names
local partNamesShortBn = {
    ['I'] = 'বিধিবদ্ধ প্রজ্ঞাপন',
    ['II'] = 'সরকারি নিয়োগ',
    ['III'] = 'প্রতিরক্ষা প্রজ্ঞাপন',
    ['IV'] = 'পেটেন্ট অফিসের প্রজ্ঞাপন',
    ['V'] = 'আইন ও বিল',
    ['VI'] = 'সুপ্রিম কোর্ট ও সরকারি কর্মকমিশনের প্রজ্ঞাপন',
    ['VII'] = 'বিবিধ অবিধিবদ্ধ প্রজ্ঞাপন',
    ['VIII'] = 'ব্যক্তিগত বিজ্ঞাপন'
}

-- Long forms for display in infobox
local partNamesLong = {
    ['I'] = 'Statutory notifications containing rules and orders issued by all Ministries and Divisions of the Government of the People\'s Republic of Bangladesh and their attached and subordinate offices and the Supreme Court of Bangladesh.',
    ['II'] = 'Notifications regarding appointments, promotions, transfers etc., issued by the Government of the People\'s Republic of Bangladesh, other than the Ministry of Defence and the Supreme Court of Bangladesh.',
    ['III'] = 'Notifications issued by the Ministry of Defence other than those included in part-I.',
    ['IV'] = 'Notifications etc., issued by the Patent office other than those included in part-I.',
    ['V'] = 'Acts, Bills etc., of the Bangladesh Parliament.',
    ['VI'] = 'Notifications issued by the Supreme Court, Accountant General Public Service Commissions and of the Attached and subordinate offices of the Government of the People\'s Republic of Bangladesh other than to those included in part-I.',
    ['VII'] = 'Non-statutory notifications issued by the minor administrations and miscellaneous notifications not included in any other part.',
    ['VIII'] = 'Advertisements and notices issued by private individuals and corporations on payment.'
}

-- Bengali long form descriptions
local partNamesLongBn = {
    ['I'] = 'গণপ্রজাতন্ত্রী বাংলাদেশ সরকারের সকল মন্ত্রণালয়, বিভাগ, সংযুক্ত ও অধীনস্থ দপ্তরসমূহ এবং বাংলাদেশ সুপ্রিম কোর্ট কর্তৃক জারীকৃত বিধি ও আদেশাবলী সম্বলিত বিধিবদ্ধ প্রজ্ঞাপনসমূহ',
    ['II'] = 'প্রতিরক্ষা মন্ত্রণালয় ও বাংলাদেশ সুপ্রিম কোর্ট ব্যতীত গণপ্রজাতন্ত্রী বাংলাদেশ সরকার কর্তৃক জারীকৃত যাবতীয় নিয়োগ, পদোন্নতি, বদলি ইত্যাদি বিষয়ক প্রজ্ঞাপনসমূহ।',
    ['III'] = 'প্রথম খণ্ডে অন্তর্ভুক্ত প্রজ্ঞাপনসমূহ ব্যতীত প্রতিরক্ষা মন্ত্রণালয় কর্তৃক জারীকৃত প্রজ্ঞাপনসমূহ।',
    ['IV'] = 'প্রথম খণ্ডে অন্তর্ভুক্ত প্রজ্ঞাপনসমূহ ব্যতীত পেটেন্ট অফিস কর্তৃক জারিকৃত প্রজ্ঞাপনসমূহ ইত্যাদি।',
    ['V'] = 'বাংলাদেশ জাতীয় সংসদের আইন, বিল ইত্যাদি।',
    ['VI'] = 'প্রথম খণ্ডে অন্তর্ভুক্ত প্রজ্ঞাপনসমূহ ব্যতীত বাংলাদেশ সুপ্রিম কোর্ট, বাংলাদেশ মহা হিসাব নিরীক্ষক ও নিয়ন্ত্রক, সরকারি কর্ম কমিশন এবং গণপ্রজাতন্ত্রী বাংলাদেশ সরকারের অধস্তন ও সংযুক্ত দপ্তরসমূহ কর্তৃক জারিকৃত প্রজ্ঞাপনসমূহ।',
    ['VII'] = 'অন্য কোনো খণ্ডে অপ্রকাশিত অধস্তন প্রশাসন কর্তৃক জারীকৃত অবিধিবদ্ধ ও বিবিধ প্রজ্ঞাপনসমূহ।',
    ['VIII'] = 'বেসরকারি ব্যক্তি এবং কর্পোরেশন কর্তৃক অর্থের বিনিময়ে জারীকৃত বিজ্ঞাপন ও নোটিশসমূহ।'
}

-- Roman numeral to Arabic conversion
local romanToArabic = {
    ['I'] = '1', ['i'] = '1',
    ['II'] = '2', ['ii'] = '2',
    ['III'] = '3', ['iii'] = '3',
    ['IV'] = '4', ['iv'] = '4',
    ['V'] = '5', ['v'] = '5',
    ['VI'] = '6', ['vi'] = '6',
    ['VII'] = '7', ['vii'] = '7',
    ['VIII'] = '8', ['viii'] = '8'
}

-- Arabic to Roman conversion (uppercase)
local arabicToRoman = {
    ['1'] = 'I',
    ['2'] = 'II',
    ['3'] = 'III',
    ['4'] = 'IV',
    ['5'] = 'V',
    ['6'] = 'VI',
    ['7'] = 'VII',
    ['8'] = 'VIII'
}

-- Month names in English
local monthNames = {
    ['01'] = 'January',
    ['02'] = 'February',
    ['03'] = 'March',
    ['04'] = 'April',
    ['05'] = 'May',
    ['06'] = 'June',
    ['07'] = 'July',
    ['08'] = 'August',
    ['09'] = 'September',
    ['10'] = 'October',
    ['11'] = 'November',
    ['12'] = 'December'
}

-- Function to normalize part input (accept roman/arabic, upper/lower case)
local function normalizePart(part)
    if not part then return nil end
    
    -- Convert to string and trim whitespace
    part = tostring(part):match('^%s*(.-)%s*$')
    
    -- If it's a roman numeral (upper or lower), convert to uppercase roman
    if romanToArabic[part] then
        return arabicToRoman[romanToArabic[part]]
    end
    
    -- If it's already arabic, convert to roman
    if arabicToRoman[part] then
        return arabicToRoman[part]
    end
    
    -- If it's already uppercase roman, return as is
    if partNamesShort[part] then
        return part
    end
    
    -- Default: return original
    return part
end

-- Function to get month name from number
local function getMonthName(monthNum)
    return monthNames[monthNum] or monthNum
end

-- Function to check if URL is from DPP
local function isDPPUrl(url)
    if not url then return false end
    return url:find('dpp%.gov%.bd') or url:find('dpp%.portal%.gov%.bd')
end

-- Function to parse date and extract year, month, and week number
local function parseDate(dateStr)
    if not dateStr then return nil, nil, nil end
    
    -- Try to match YYYY-MM-DD format
    local year, month, day = dateStr:match('^(%d%d%d%d)%-(%d%d)%-(%d%d)')
    
    -- If not found, try YYYY-MM-DD HH:MM:SS format
    if not year then
        year, month, day = dateStr:match('^(%d%d%d%d)%-(%d%d)%-(%d%d)%s')
    end
    
    if not year then return nil, nil, nil end
    
    -- Calculate week number (ISO week date)
    local weekNum = nil
    if year and month and day then
        -- Use os.time to convert to timestamp
        local timestamp = os.time{year=tonumber(year), month=tonumber(month), day=tonumber(day)}
        -- Get ISO week number using date formatting
        weekNum = os.date('%V', timestamp)
    end
    
    return year, month, weekNum
end

-- Function to calculate week of the month (1-5)
local function getWeekOfMonth(dateStr)
    if not dateStr then return nil end
    
    local year, month, day = dateStr:match('^(%d%d%d%d)%-(%d%d)%-(%d%d)')
    if not year or not month or not day then return nil end
    
    -- Calculate which week of the month (1-5)
    day = tonumber(day)
    return math.ceil(day / 7)
end

-- Function to convert English numbers to Bengali
local function toBengaliNumber(num)
    if not num then return num end
    local bengaliDigits = {['0']='০', ['1']='১', ['2']='২', ['3']='৩', ['4']='৪', 
                          ['5']='৫', ['6']='৬', ['7']='৭', ['8']='৮', ['9']='৯'}
    return (tostring(num):gsub('[0-9]', bengaliDigits))
end

-- Function to convert Bengali numbers to English
local function toEnglishNumber(num)
    if not num then return num end
    local englishDigits = {['০']='0', ['১']='1', ['২']='2', ['৩']='3', ['৪']='4',
                          ['৫']='5', ['৬']='6', ['৭']='7', ['৮']='8', ['৯']='9'}
    return (tostring(num):gsub('[০-৯]', englishDigits))
end

-- Function to get ordinal suffix in English
local function getOrdinalEn(num)
    num = tonumber(toEnglishNumber(num))
    if not num then return '' end
    local suffix = 'th'
    local lastDigit = num % 10
    local lastTwoDigits = num % 100
    if lastTwoDigits < 11 or lastTwoDigits > 13 then
        if lastDigit == 1 then suffix = 'st'
        elseif lastDigit == 2 then suffix = 'nd'
        elseif lastDigit == 3 then suffix = 'rd'
        end
    end
    return tostring(num) .. suffix
end

-- Function to get ordinal in Bengali (using ১ম, ২য়, ৩য়, ৪র্থ pattern)
local function getOrdinalBn(num)
    num = tonumber(toEnglishNumber(num))
    if not num then return '' end
    local bengaliNum = toBengaliNumber(num)
    local suffix = 'ম'
    local lastDigit = num % 10
    if lastDigit == 1 and num ~= 11 then suffix = 'ম'
    elseif lastDigit == 2 and num ~= 12 then suffix = 'য়'
    elseif lastDigit == 3 and num ~= 13 then suffix = 'য়'
    elseif lastDigit == 4 and num ~= 14 then suffix = 'র্থ'
    elseif lastDigit == 6 and num ~= 16 then suffix = 'ষ্ঠ'
    else suffix = 'ম' end
    return bengaliNum .. suffix
end

-- Function to get categories for weekly gazettes
local function getWeeklyCategories(args, pageName)
    local cats = {}
    local year = args.year
    local month = args.month
    local part = normalizePart(args.part)
    
    -- Auto-extract year and month from date if not provided
    if args.date and (not year or not month) then
        local parsedYear, parsedMonth, _ = parseDate(args.date)
        year = year or parsedYear
        month = month or parsedMonth
    end
    
    -- Don't categorize the main template or documentation pages
    if pageName and (pageName == 'Bangladesh Gazette Information' or pageName:find('/doc$') or pageName:find('/testcases$')) then
        return ''
    end
    
    if not year or not month or not part then
        return '[[Category:Bangladesh Gazette files needing parameters]]'
    end
    
    local monthName = getMonthName(month)
    local partName = partNamesShort[part] or 'Part ' .. part
    
    -- 1. Bangladesh Weekly Gazette from [Month] [Year] [Part Name]
    table.insert(cats, '[[Category:Bangladesh Weekly Gazette ' .. year .. ' - ' .. partName .. ']]')
    
    return table.concat(cats, '\n')
end

-- Function to get categories for extraordinary gazettes
local function getExtraordinaryCategories(args, pageName)
    local cats = {}
    local year = args.year
    local month = args.month
    local ministry = args.ministry or args.organization
    
    -- Auto-extract year and month from date if not provided
    if args.date and (not year or not month) then
        local parsedYear, parsedMonth, _ = parseDate(args.date)
        year = year or parsedYear
        month = month or parsedMonth
    end
    
    -- Don't categorize the main template or documentation pages
    if pageName and (pageName == 'Bangladesh Gazette Information' or pageName:find('/doc$') or pageName:find('/testcases$')) then
        return ''
    end
    
    if not year or not month or not ministry then
        return '[[Category:Bangladesh Gazette files needing parameters]]'
    end
    
    local monthName = getMonthName(month)
    
    -- Only one category for extraordinary gazettes:
    -- Bangladesh Extraordinary Gazette from [Month] [Year] by [Ministry]
    table.insert(cats, '[[Category:Bangladesh Extraordinary Gazette from ' .. monthName .. ' ' .. year .. ' by ' .. ministry .. ']]')
    
    return table.concat(cats, '\n')
end

-- Helper: get a single best property value from a Wikidata entity
local function getProperty(entity, prop)
    return (core.parseStatements(entity:getBestStatements(prop), nil) or {nil})[1]
end

-- Helper: get all best property values from a Wikidata entity
local function getBestProperties(entity, prop)
    return core.parseStatements(entity:getBestStatements(prop), nil)
end

-- Harvest Wikidata data relevant to the gazette header
local function harvest_gazette_wikidata(itemID, lang, namespace, pagename)
    local data = {}
    local cats = ''
    if not (mw.wikibase and itemID) then
        return data, cats
    end
    local entity = mw.wikibase.getEntity(itemID)
    if not entity then
        cats = '[[Category:Bangladesh Gazette files with bad Wikidata link]]'
        return data, cats
    end
    if entity.id ~= itemID then
        cats = '[[Category:Bangladesh Gazette files with redirected Wikidata link]]'
    end

    -- Title from P1476
    local titleVals = {}
    if entity.statements and entity.statements.P1476 then
        for _, statement in pairs(entity:getBestStatements('P1476')) do
            if statement.mainsnak.snaktype == 'value' then
                local val = statement.mainsnak.datavalue.value
                titleVals[val.language] = val.text
            end
        end
    end
    data.title_ = titleVals
    local langList = mw.language.getFallbacksFor(lang or 'en')
    table.insert(langList, 1, lang or 'en')
    for _, l in ipairs(langList) do
        if titleVals[l] then data.title = titleVals[l]; break end
    end

    -- Label fallback
    data.label = getLabel(entity, lang or 'en')
    data.labels = {}
    if entity.labels then
        for l, val in pairs(entity.labels) do
            data.labels[l] = val.value
            data.label = data.label or data.labels[l]
        end
    end

    -- Name to display in header
    data.name = data.title or data.label

    -- Authority control
    local AC_cats
    data.authority, AC_cats = authorityControl(entity, {wikidata = itemID}, lang or 'en', 5)
    local _, nIdentifiers = string.gsub(data.authority or '', '*', '')
    if nIdentifiers <= 1 then
        data.authority = nil
        AC_cats = ''
    end
    if not (namespace == 2 or namespace == 828 or math.fmod(namespace, 2) == 1) then
        cats = cats .. (AC_cats or '')
    end

    -- Wikisource and wikiquote sitelinks
    local projects = {s='wikisource', q='wikiquote'}
    for code, project in pairs(projects) do
        local sitelinks = getSitelinks(entity, project)
        if sitelinks then
            local lng, _ = next(sitelinks)
            table.insert(langList, lng)
            for _, language in ipairs(langList) do
                local sitelink = sitelinks[language]
                if sitelink then
                    data[project] = string.format('%s:%s:%s', code, language, sitelink)
                    break
                end
            end
        end
    end

    -- Wikisource index fallback (P1957)
    local wikisource_index = getProperty(entity, 'P1957')
    data.wikisource = data.wikisource or wikisource_index

    -- Wikipedia sitelinks (for the header link icon)
    local wpLinks = getSitelinks(entity, 'wikipedia')
    if wpLinks then
        for _, language in ipairs(langList) do
            if wpLinks[language] then
                data.wikipedia = string.format('w:%s:%s', language, wpLinks[language])
                break
            end
        end
    end

    -- Commons category (P373)
    data.homecat = getProperty(entity, 'P373')

    -- Descriptions
    data.descriptions = {}
    if entity.descriptions then
        for l, val in pairs(entity.descriptions) do
            data.descriptions[l] = 1
        end
    end

    return data, cats
end

-- Main function to generate infobox
function p.infobox(frame)
    local args = frame:getParent().args
    local pageName = mw.title.getCurrentTitle().text
    
    -- Language
    local lang = args.lang or 'en'
    local namespace = mw.title.getCurrentTitle().namespace

    -- Wikidata parameter
    local wikidataID = args.wikidata
    local wdCats = ''
    local wdData = {}
    if wikidataID and wikidataID ~= '' then
        wdData, wdCats = harvest_gazette_wikidata(wikidataID, lang, namespace, pageName)
    else
        wdCats = '[[Category:Bangladesh Gazette files without Wikidata item]]'
    end

    -- Determine gazette type
    local gazetteType = args.type or 'extraordinary'
    gazetteType = gazetteType:lower()
    
    -- Validate gazette type, default to extraordinary if invalid
    if gazetteType ~= 'weekly' and gazetteType ~= 'extraordinary' then
        gazetteType = 'extraordinary'
    end
    
    -- Build ordered fields array
    local orderedFields = {}
    
    -- Helper function to add field
    local function addField(name, value)
        if value and value ~= '' then
            table.insert(orderedFields, {name = name, value = value})
        end
    end
    
    -- 1. GAZETTE TYPE (always first)
    local gazetteTypeLabel = frame:expandTemplate{
        title = 'LangSwitch',
        args = {en='Gazette Type', bn='গেজেটের ধরণ', default='Gazette Type'}
    }
    if gazetteType == 'weekly' then
        local gazetteTypeValue = frame:expandTemplate{
            title = 'en',
            args = {'Weekly'}
        } .. ' ' .. frame:expandTemplate{
            title = 'bn',
            args = {'সাপ্তাহিক'}
        }
        addField(gazetteTypeLabel, gazetteTypeValue)
    else
        local gazetteTypeValue = frame:expandTemplate{
            title = 'en',
            args = {'Extraordinary'}
        } .. ' ' .. frame:expandTemplate{
            title = 'bn',
            args = {'অতিরিক্ত সংখ্যা'}
        }
        addField(gazetteTypeLabel, gazetteTypeValue)
    end
    
    -- 2. DESCRIPTION (don't add to orderedFields - handled by Information template)
    local description = nil
    local partField = nil  -- We'll add Part right after description
    
    if args.description and args.description ~= '' then
        description = args.description
    elseif gazetteType == 'weekly' and args.part then
        local normalizedPart = normalizePart(args.part)
        local partLongEn = partNamesLong[normalizedPart]
        local partLongBn = partNamesLongBn[normalizedPart]
        
        if partLongEn and partLongBn then
            description = frame:expandTemplate{
                title = 'en',
                args = {partLongEn}
            } .. ' ' .. frame:expandTemplate{
                title = 'bn',
                args = {partLongBn}
            }
        end
    end
    
    -- Build Part field separately for weekly gazettes
    if gazetteType == 'weekly' and args.part then
        local normalizedPart = normalizePart(args.part)
        local partLabel = frame:expandTemplate{
            title = 'LangSwitch',
            args = {en='Part', bn='খণ্ড', default='Part'}
        }
        
        if partNamesLong[normalizedPart] then
            local partLongName = partNamesLong[normalizedPart]
            local partValueEn = 'Part ' .. normalizedPart .. ': ' .. partLongName
            local partNumBn = toBengaliNumber(romanToArabic[normalizedPart])
            local partValueBn = 'খণ্ড ' .. partNumBn .. ': ' .. partNamesLongBn[normalizedPart]
            local partValue = frame:expandTemplate{
                title = 'en',
                args = {partValueEn}
            } .. ' ' .. frame:expandTemplate{
                title = 'bn',
                args = {partValueBn}
            }
            partField = frame:expandTemplate{
                title = 'Information field',
                args = {name=partLabel, value=partValue}
            }
        else
            partField = frame:expandTemplate{
                title = 'Information field',
                args = {name=partLabel, value=args.part}
            }
        end
    end
    
    -- 4. DATE (don't add to orderedFields - handled by Information template)
    local dateValue = args.date
    
    -- Continue with WEEKLY-specific fields
    if gazetteType == 'weekly' then
        -- 5. VOLUME (weekly only)
        local volumeNum = args.volume
        if not volumeNum or volumeNum == '' then
            if args.date then
                local _, _, weekNum = parseDate(args.date)
                volumeNum = weekNum
            end
        end
        if volumeNum then
            local volumeLabel = frame:expandTemplate{
                title = 'LangSwitch',
                args = {en='Volume', bn='খণ্ড সমূহ', default='Volume'}
            }
            local volumeValueEn = getOrdinalEn(volumeNum)
            local volumeValueBn = getOrdinalBn(volumeNum)
            local volumeValue = frame:expandTemplate{
                title = 'LangSwitch',
                args = {en=volumeValueEn, bn=volumeValueBn, default=volumeValueEn}
            }
            addField(volumeLabel, volumeValue)
        end
        
        -- 6. WEEK (weekly only)
        local weekOfMonth = args.week
        if not weekOfMonth or weekOfMonth == '' then
            if args.date then
                weekOfMonth = getWeekOfMonth(args.date)
            end
        end
        if weekOfMonth then
            local weekLabel = frame:expandTemplate{
                title = 'LangSwitch',
                args = {en='Week', bn='সপ্তাহ', default='Week'}
            }
            local weekValueEn = getOrdinalEn(weekOfMonth)
            local weekValueBn = getOrdinalBn(weekOfMonth)
            local weekValue = frame:expandTemplate{
                title = 'LangSwitch',
                args = {en=weekValueEn, bn=weekValueBn, default=weekValueEn}
            }
            addField(weekLabel, weekValue)
        end
    end
    
    -- 7. SOURCE (don't add to orderedFields - handled by Information template)
    local sourceValue = nil
    if args.source or args.source_url then
        local rawSource = args.source or args.source_url
        if isDPPUrl(rawSource) then
            sourceValue = frame:expandTemplate{
                title = 'Source-DPP',
                args = {rawSource}
            } .. ' ' .. frame:expandTemplate{
                title = 'Institution:Department of Printing and Publications'
            }
        else
            sourceValue = rawSource
        end
    end
    
    -- 8. ORGANIZATION (extraordinary only, after source)
    if gazetteType == 'extraordinary' then
        if args.ministry or args.organization then
            local orgLabel = frame:expandTemplate{
                title = 'LangSwitch',
                args = {en='Organization', bn='সংস্থা', default='Organization'}
            }
            addField(orgLabel, args.ministry or args.organization)
        end
    end
    
    -- 9. AUTHOR (don't add to orderedFields - handled by Information template)
    local authorValue = args.author
    if not authorValue or authorValue == '' then
        authorValue = frame:expandTemplate{
            title = 'Institution:Government of Bangladesh'
        }
    end
    
    -- 10. PAGE RANGE (before permission and other_versions)
    if args.page_range then
        local pageRangeLabel = frame:expandTemplate{
            title = 'LangSwitch',
            args = {en='Page Range', bn='পৃষ্ঠা', default='Page Range'}
        }
        local pageRangeEn = toEnglishNumber(args.page_range)
        local pageRangeBn = toBengaliNumber(pageRangeEn)
        local pageRangeValue = frame:expandTemplate{
            title = 'LangSwitch',
            args = {en=pageRangeEn, bn=pageRangeBn, default=pageRangeEn}
        }
        addField(pageRangeLabel, pageRangeValue)
    end
    
    -- 11. PERMISSION (don't add to orderedFields - handled by Information template)
    local permissionValue = args.permission
    
    -- 12. OTHER VERSIONS (don't add to orderedFields - handled by Information template)
    local otherVersionsValue = args.other_versions
    
    -- Build other_fields from ordered fields
    -- Strategy: We need to work around Module:Information's fixed field order
    -- Since we can't insert between author and permission, we'll handle everything in source/author fields
    local otherFields0 = {}
    local sourceFields = {}  -- Will be appended to source
    local authorFields = {}  -- Will be appended to author
    
    for _, field in ipairs(orderedFields) do
        local fieldHtml = frame:expandTemplate{
            title = 'Information field',
            args = {name=field.name, value=field.value}
        }
        
        -- Gazette Type goes in other_fields_0 (before description)
        if field.name:find('Gazette Type') or field.name:find('গেজেটের ধরণ') then
            table.insert(otherFields0, fieldHtml)
        -- Part goes after Description (we'll handle this specially)
        elseif field.name:find('Part') and not field.name:find('Page') then
            -- Part will be added to description field directly
        -- Organization goes in sourceFields (appended after source)
        elseif field.name:find('Organization') or field.name:find('সংস্থা') then
            table.insert(sourceFields, fieldHtml)
        -- Volume, Week, Page Range go in authorFields (appended after author)
        else
            table.insert(authorFields, fieldHtml)
        end
    end
    
    -- Build Information template arguments
    local infoArgs = {}
    if #otherFields0 > 0 then
        infoArgs.other_fields_0 = table.concat(otherFields0, '\n')
    end
    if description then 
        infoArgs.description = description 
        -- Add Part field right after description for weekly gazettes
        if partField then
            infoArgs.description = description .. '\n' .. partField
        end
    end
    if dateValue then infoArgs.date = dateValue end
    if sourceValue then 
        infoArgs.source = sourceValue 
        -- Add Organization after source for extraordinary gazettes
        if #sourceFields > 0 then
            infoArgs.source = sourceValue .. '\n' .. table.concat(sourceFields, '\n')
        end
    end
    if authorValue then 
        infoArgs.author = authorValue 
        -- Add Volume, Week, Page Range after author
        if #authorFields > 0 then
            infoArgs.author = authorValue .. '\n' .. table.concat(authorFields, '\n')
        end
    end
    if permissionValue and permissionValue ~= '' then infoArgs.permission = permissionValue end
    if otherVersionsValue and otherVersionsValue ~= '' then infoArgs['other versions'] = otherVersionsValue end
    
    -- Expand the Information template
    local output = frame:expandTemplate{
        title = 'Information',
        args = infoArgs
    }
    
    -- Add categories
    output = output .. '\n'
    if gazetteType == 'weekly' then
        output = output .. getWeeklyCategories(args, pageName)
    else
        output = output .. getExtraordinaryCategories(args, pageName)
    end
    
    -- Build header (blue box) only if wikidata parameter is provided
    if wikidataID and wikidataID ~= '' then
        local headerArgs = {
            wikidata    = wikidataID,
            wikisource  = wdData.wikisource,
            wikiquote   = wdData.wikiquote,
            wikipedia   = wdData.wikipedia,
            homecat     = wdData.homecat,
            authority   = wdData.authority,
            name        = wdData.name,
            lang        = lang,
            namespace   = namespace,
            infobox     = 'book',
            QS          = nil,
            no_qs       = true,
        }
        local header = artcore.build_html(headerArgs)
        output = header .. '\n' .. output
    end

    output = output .. '\n' .. wdCats

    return output
end

-- Function to check if Bangladesh Gazette Information template is used on page
function p.hasInfobox(frame)
    local pageContent = mw.title.getCurrentTitle():getContent()
    if pageContent and pageContent:find('{{Bangladesh Gazette Information', 1, true) then
        return 'yes'
    end
    return 'no'
end

-- ============================================================
-- Category navigation / categorization for Weekly Gazette cats
-- ============================================================

-- Reverse lookup: part short name → Roman numeral (for validation)
local partNameToRoman = {}
for roman, name in pairs(partNamesShort) do
    partNameToRoman[name] = roman
end

-- Parse the category page name to determine what kind of Weekly Gazette category it is.
-- Returns: kind ('leaf'|'year'|'part'), year (string|nil), partName (string|nil)
local function parseWeeklyGazetteCategoryName(pageName)
    -- Pattern: "Bangladesh Weekly Gazette YYYY - Part Name"
    -- e.g.    "Bangladesh Weekly Gazette 2023 - Supreme Court and others"
    local year, partName = pageName:match(
        '^Bangladesh Weekly Gazette (%d%d%d%d) %- (.+)$'
    )
    if year and partName then
        return 'leaf', year, partName
    end

    -- Pattern: "Bangladesh Weekly Gazette YYYY"
    local yearOnly = pageName:match('^Bangladesh Weekly Gazette (%d%d%d%d)$')
    if yearOnly then
        return 'year', yearOnly, nil
    end

    -- Pattern: "Bangladesh Weekly Gazette - Part Name"
    local partOnly = pageName:match('^Bangladesh Weekly Gazette %- (.+)$')
    if partOnly then
        return 'part', nil, partOnly
    end

    return nil, nil, nil
end

-- Entry point for {{Bangladesh Weekly Gazette Navigatio}}
function p.weeklyGazetteNavigation(frame)
    local title    = mw.title.getCurrentTitle()
    local pageName = title.text          -- text without namespace prefix
    local namespace = title.namespace    -- 14 = Category namespace

    -- Suppress on doc/testcase subpages
    if pageName:find('/doc$') or pageName:find('/testcases$') then
        return ''
    end

    -- Only act in Category namespace (14); elsewhere just warn
    if namespace ~= 14 then
        return '[[Category:Bangladesh Gazette template errors]]'
    end

    local kind, year, partName = parseWeeklyGazetteCategoryName(pageName)

    if kind == 'leaf' then
        -- e.g. "Bangladesh Weekly Gazette 2023 - Supreme Court and others"
        -- Add to both the year cat and the part cat
        return string.format(
            '[[Category:Bangladesh Weekly Gazette %s]][[Category:Bangladesh Weekly Gazette - %s]]',
            year, partName
        )

    elseif kind == 'year' then
        -- e.g. "Bangladesh Weekly Gazette 2023"
        return '[[Category:Bangladesh Weekly Gazette by year]]'

    elseif kind == 'part' then
        -- e.g. "Bangladesh Weekly Gazette - Supreme Court and others"
        return '[[Category:Bangladesh Weekly Gazette by part]]'

    else
        -- Unrecognised format
        return '[[Category:Bangladesh Gazette files needing parameters]]'
    end
end

return p