Difference between revisions of "User:WindBOT/Filters"

From Team Fortress Wiki
Jump to: navigation, search
(Item names: Exception for "classified")
m (Moved links: testing)
 
(549 intermediate revisions by 20 users not shown)
Line 4: Line 4:
 
If the bot is malfunctioning, chances are that the problem lies in one of these blocks of code. Thus, instead of shutting down the whole bot, it would be wiser to disable only the chunk of code that is misbehaving.
 
If the bot is malfunctioning, chances are that the problem lies in one of these blocks of code. Thus, instead of shutting down the whole bot, it would be wiser to disable only the chunk of code that is misbehaving.
 
To make the bot ignore a certain line, add a "#" in front of it:
 
To make the bot ignore a certain line, add a "#" in front of it:
  # This line will be ignored
+
# This line will be ignored
If there are multiple lines, wrap them inside triple-quotes ('''you still need to put the two spaces at the beginning of the line''':
+
If there are multiple lines, wrap them inside triple-quotes ('''you still need to put the two spaces at the beginning of the line'''):
  """This line will be ignored
+
"""This line will be ignored
  and this one as well
+
and this one as well
  and this one is cake
+
and this one is cake
  and the previous one was a lie but it was still ignored"""
+
and the previous one was a lie but it was still ignored"""
 
If all else fails, you can simply delete the block from the page. The bot can't come up with code by itself yet, so it won't run anything.
 
If all else fails, you can simply delete the block from the page. The bot can't come up with code by itself yet, so it won't run anything.
 
Or, if the problem really is elsewhere, [{{fullurl:Special:Block|wpBlockAddress={{BASEPAGENAMEE}}&wpBlockExpiry=infinite&wpAnonOnly=0&wpEnableAutoblock=0&wpCreateAccount=0&wpBlockReason=Bot%20gone%20crazy:%20}} block the bot].
 
Or, if the problem really is elsewhere, [{{fullurl:Special:Block|wpBlockAddress={{BASEPAGENAMEE}}&wpBlockExpiry=infinite&wpAnonOnly=0&wpEnableAutoblock=0&wpCreateAccount=0&wpBlockReason=Bot%20gone%20crazy:%20}} block the bot].
Line 16: Line 16:
  
 
== Global filters ==
 
== Global filters ==
=== File categorization (Add {{tl|No category}}) ===
+
=== File categorization (Add {{tl|No category}} or {{tl|User image}}) ===
  def fileCategorization(content, **kwargs):
+
def fileCategorization(content, **kwargs):
  badLoneCategories = [u'Category:Screenshot images', u'Category:Artwork images', u'Category:Multimedia files', u'Category:Extracted media', u'Category:Images without license tags', u'Category:Images that need improving', u'Category:Images using ((Another license)) incorrectly']
+
badLoneCategories = [u'Category:Screenshot images', u'Category:Artwork images', u'Category:Multimedia files', u'Category:Extracted media', u'Category:Images without license tags', u'Category:Images that need improving', u'Category:Images using ((Another license)) incorrectly']
  extraStuff = <nowiki>u'{{No category}}'</nowiki>
+
extraStuff = <nowiki>u'{{No category}}'</nowiki>
  regLang = compileRegex('/[^/]+$')
+
userImageCategory = <nowiki>u'[[Category:User images]]'</nowiki>
  if 'article' not in kwargs or content.find(u'#REDIRECT ') != -1:
+
userAudioCategory = <nowiki>u'[[Category:User audio]]'</nowiki>
  return content
+
userTextCategory = <nowiki>u'[[Category:User text]]'</nowiki>
  title = u(kwargs['article'].title)
+
regLang = compileRegex('/[^/]+$')
  if title[:5].lower() != u'file:':
+
if 'article' not in kwargs or content.lower().find(u'#redirect') != -1:
  return content
+
return content
  for c in kwargs['article'].getCategories():
+
title = u(kwargs['article'].title)
  if u(regLang.sub(<nowiki>u''</nowiki>, c)) not in badLoneCategories:
+
if title[:5].lower() != u'file:':
  return content
+
return content
  if not len(content):
+
categories = kwargs['article'].getCategories()
  return extraStuff
+
if title.lower().startswith(u'file:user '):
  if content.find(extraStuff) != -1:
+
if title.lower().endswith(('.mp3', '.wav', '.ogg', '.flac')) and u'Category:User audio' not in categories and userAudioCategory not in content:
  return content
+
return (content.strip() + u'\n' + userAudioCategory).strip()
  return content.strip() + u'\n' + extraStuff
+
if title.lower().endswith(('.txt', '.cfg', '.diff', '.patch')) and u'Category:User text' not in categories and userTextCategory not in content:
  addFilter(fileCategorization)
+
return (content.strip() + u'\n' + userTextCategory).strip()
 +
if title.lower().endswith(('.png', '.gif', '.jpg', '.jpeg', '.webp', '.apng', '.psd', '.webm')) and u'Category:User images' not in categories and userImageCategory not in content:
 +
return (content.strip() + u'\n' + userImageCategory).strip()
 +
return content # Don't go further
 +
for c in categories:
 +
if u(regLang.sub(<nowiki>u''</nowiki>, c)) not in badLoneCategories:
 +
return content
 +
if not len(content):
 +
return extraStuff
 +
if content.find(extraStuff) != -1:
 +
return content
 +
return content.strip() + u'\n' + extraStuff
 +
addFilter(fileCategorization)
  
 
== Page filters ==
 
== Page filters ==
  addPageFilter(r'^user:', r'(?:talk|help|wiki|template):')
+
addPageFilter(r'^user:', r'(?:talk|help|wiki|template|module):')
  
 
== Semantic filters ==
 
== Semantic filters ==
 
=== Item names ===
 
=== Item names ===
  categories = [ # These are categories that contain that name of things that should be capitalized
+
categories = [ # These are categories that contain that name of things that should be capitalized
      'Weapons',
+
    'Weapons',
      'Hats',
+
    'Hats',
      'Miscellaneous items',
+
    'Miscellaneous items',
      'Buildings'
+
    'Buildings'
  ]
+
]
  exceptions = [ # Those are page names that are in the above categories but should not count as capitalized weapon names
+
exceptions = [ # Those are page names that are in the above categories but should not count as capitalized weapon names
      'Direct Hit',
+
    'Direct Hit',
      'Hats', 'Weapons', 'Buildings',
+
    'Hats', 'Weapons', 'Miscellaneous items', 'Buildings',
      'Item levels',
+
    'Item levels',
      'Developer weapons',
+
    'Developer weapons',
      'Self-made items',
+
    'Self-made items',
      'Unused content',
+
    'Unused content',
      'Promotional items',
+
    'Promotional items',
      'Mercenary', 'Taunts', 'Decapitation', 'Fists', # Too common to be reliably replaced
+
    'Mercenary', 'Taunts', 'Decapitation', 'Fists', 'Cloak', 'One-Man Army', # Too common to be reliably replaced
      'Club', 'Bat', 'Knife', 'Syringe', 'Bottle', # Ditto (weapons)
+
    'Club', 'Bat', 'Knife', 'Syringe', 'Bottle', 'Original', # Ditto (weapons)
      'Classified', # Not capitalized even when referring to the hat
+
    'Classified', # Not capitalized even when referring to the hat
      'Attendant', # Avoids issue with French
+
    'Attendant', # Avoids issue with French
      'Buff Banner', 'Medi Gun' # Avoids issue with German
+
    'Buff Banner', 'Medi Gun', 'Bonk Helm', 'B.A.S.E. Jumper', # Avoids issue with German
  ]
+
    'Voodoo Juju', # Conflicts Juju/JuJu
  for c in categories:
+
    'Reskins' # Not capitalized
      c = wikitools.category.Category(wiki(), u(c))
+
]
      for p in pageFilter(c.getAllMembers(titleonly=True)):
+
for c in categories:
          if p not in exceptions:
+
    c = wikitools.category.Category(wiki(), u(c))
              enforceCapitalization(p)
+
    for p in pageFilter(c.getAllMembers(titleonly=True)):
  # Special case: Direct Hit
+
        if p not in exceptions:
  addSafeFilter(wordFilter('Direct Hit', r'(?<!a )(?<!one )(?<!on )(?<!\()(?<!placed )(?<!single )\bDirect Hit'))
+
            enforceCapitalization(p)
  # Special case: Attendant
+
# Special case: Direct Hit
  enforceCapitalization('Attendant', languageBlacklist=['fr'])
+
addSafeFilter(wordFilter('Direct Hit', r'(?<!a )(?<!one )(?<!on )(?<!\()(?<!placed )(?<!single )\bDirect Hit'))
  # Special cases: "Buff-Banner"/"Medigun" in German
+
# Special case: Attendant
  enforceCapitalization('Buff Banner', 'Medi Gun', languageBlacklist=['de'])
+
enforceCapitalization('Attendant', languageBlacklist=['fr'])
  enforceCapitalization('Buff-Banner', 'Medigun', language='de')
+
# Special cases: "Buff-Banner"/"Medigun" in German
  # Put "an" in front of Electro Sapper
+
enforceCapitalization('Buff Banner', 'Medi Gun', languageBlacklist=['de'])
  addSafeFilter(
+
enforceCapitalization('Buff-Banner', 'Medigun', language='de')
      dumbReplace('a Electro Sapper', 'an Electro Sapper'),
+
# Put "an" in front of Electro Sapper
      dumbReplace('A Electro Sapper', 'An Electro Sapper')
+
addSafeFilter(
  )
+
    dumbReplace('an Sapper', 'a Sapper'),
  # Flare Gun
+
    dumbReplace('An Sapper', 'A Sapper')
  addSafeFilter(wordFilter('Flare Gun', 'flaregun'))
+
)
 +
# Flare Gun
 +
addSafeFilter(wordFilter('Flare Gun', 'flaregun'))
  
 
=== Classes ===
 
=== Classes ===
  # Not "Heavy" because the word can be used as an adjective:
+
# Not "Heavy" because the word can be used as an adjective:
  classes = ['Scout', 'Soldier', 'Pyro', 'Demoman', 'Heavy Weapons Guy', 'Engineer', 'Medic', 'Sniper'] # Spy added later
+
classes = ['Scout', 'Soldier', 'Pyro', 'Demoman', 'Heavy Weapons Guy', 'Engineer', 'Medic', 'Sniper'] # Spy added later
  classesPlurals = ['Scouts', 'Pyros', 'Demomen', 'Heavy Weapons Guys', 'Heavies', 'Engineers', 'Snipers', 'Spies']
+
classesPlurals = ['Scouts', 'Pyros', 'Demomen', 'Heavy Weapons Guys', 'Heavies', 'Engineers', 'Snipers', 'Spies']
  enforceCapitalization(*classes)
+
enforceCapitalization(*classes)
  enforceCapitalization(*classesPlurals)
+
enforceCapitalization(*classesPlurals)
  # Special Spy case:
+
# Special Spy case:
  addSafeFilter(wordFilter('Spy', '(?<!I )Spy'))
+
addSafeFilter(wordFilter('Spy', '(?<!I )Spy'))
  classes.append('Spy') # Used in later functions with Spy included
+
classes.append('Spy') # Used in later functions with Spy included
 
 
=== BLU/RED ===
 
'''DISABLED''' because of TFC's team names
 
  """addSafeFilter(wordFilter('BLU', 'BLU(?![- ]+ray)'))
 
  # Can't force simply "red" because it would ruin things like "this red hat" or something. However, the following can probably be assumed to be correct:
 
  for c in classes + classesPlurals + ['Soldiers']:
 
      enforceCapitalization('RED ' + c) # Capitalize "RED Scout", "RED Demoman", etc
 
  enforceCapitalization('RED team', 'RED Heavy', 'RED Intelligence')
 
  # Similarly, "blue" can be converted to "BLU" in the same cases:
 
  for c in classes + classesPlurals + ['Soldiers']:
 
      addSafeFilter(wordFilter('BLU ' + c, 'blue? ' + c)) # Convert "blue scout" -> "BLU Scout", "blue demoman" -> "BLU Demoman", etc.
 
  addSafeFilter(
 
      wordFilter('BLU team', 'blue? team'),
 
      wordFilter('BLU Heavy', 'blue? Heavy'),
 
      wordFilter('BLU Intelligence', 'blue? intelligence')
 
  )"""
 
  
 
=== Other capitalized words ===
 
=== Other capitalized words ===
  enforceCapitalization('Team Fortress 2', 'TF2')
+
enforceCapitalization('Team Fortress 2', 'TF2')
  enforceCapitalization('Payload', 'Cloak', 'Overtime', 'Dispensers', 'Sappers')
+
enforceCapitalization('Payload', 'Overtime', 'Dispensers', 'Sappers', 'Crits', 'Crit')
  enforceCapitalization('PlayStation', 'Xbox')
+
enforceCapitalization('PlayStation', 'Xbox')
  enforceCapitalization('iPod', 'iPhone')
+
enforceCapitalization('iPod', 'iPhone')
  
 
=== Word aliases ===
 
=== Word aliases ===
  addSafeFilter(
+
addSafeFilter(
      wordFilter(u'Übersaw', u'[üu]bersaw'), # Ubersaw
+
    wordFilter(u'Ubersaw', u'[üÜ]bersaw'), # Ubersaw
      wordFilter(u'Force-a-Nature', u'Force of Nature'), # Force-A-Nature
+
    wordFilter(u'Force-a-Nature', u'Force of Nature'), # Force-A-Nature
      wordFilter(u'ÜberCharge', u'[Üüu]ber(?!säge)(?!sage)(?: ?charge)?'), # ÜberCharge
+
    wordFilter(u'ÜberCharges', u'[Üüu]ber(?!säge)(?!sage)(?! Entertainment)(?: ?charge)?s'), # ÜberCharges
      wordFilter(u'ÜberCharges', u'[Üüu]ber(?!säge)(?!sage)(?: ?charge)?s'), # ÜberCharges
+
    wordFilter(u'Intelligence', u'Intel(?!\\s*(?:CPU|processor))'), # Intelligence (maybe adding "flag" in there would be too agressive)
      wordFilter(u'Intelligence', u'Intel(?!\\s*(?:CPU|processor))'), # Intelligence (maybe adding "flag" in there would be too agressive)
+
    wordFilter(u'Sapper', u'(?:Ele[ck]tro |Ultra )+Sapper', u'Electro-?Sapper'),
      wordFilter(u'Electro Sapper', u'(?<!Elektro.)(?<!Electro.)(?<!Ultra.)Sapper', u'Electro-?Sapper'),
+
    wordFilter(u'Heavy Weapons Guy', u'Heavy Guy'),
      wordFilter(u'Heavy Weapons Guy', u'Heavy Guy'),
+
    wordFilter(u'Intelligence room', u'Intel(?:ligence)? room'),
      wordFilter(u'Intelligence room', u'Intel(?:ligence)? room'),
+
    wordFilter(u'Intelligence briefcase', u'Intel(?:ligence)? briefcase'),
      wordFilter(u'Intelligence briefcase', u'Intel(?:ligence)? briefcase'),
+
    wordFilter(u'K.G.B.', u'KGB'),
      wordFilter(u'K.G.B.', u'KGB'),
+
    wordFilter(u'Chieftain', u'Chieftian', u'Chieftan'),
      wordFilter(u'Chieftain', u'Chieftian', u'Chieftan'),
+
    wordFilter(u'Batallion\'s Backup', u'Batallion\'?s? Back-?up'),
      wordFilter(u'Batallion\'s Backup', u'Batallion\'?s? Back-?up'),
+
    wordFilter(u'Mann-Conomy', u'Mann?-?Conomy'),
      wordFilter(u'Mann-Conomy', u'Mann?-?Conomy'),
+
    wordFilter(u'First-person view', u'1(?:st)? person view'),
      wordFilter(u'First-person view', u'1(?:st)? person view')
+
    wordFilter(u'Über Update', u'[UÜü]ber update')
      #wordFilter(u'Stickybomb', u'Stickybomb', u'Sticky bomb'),
+
    #wordFilter(u'Stickybomb', u'Stickybomb', u'Sticky bomb'),
      #wordFilter(u'Stickybombs', u'Stickybombs', u'Sticky bombs')
+
    #wordFilter(u'Stickybombs', u'Stickybombs', u'Sticky bombs')
  )
+
)
  addSafeFilter(
+
addSafeFilter(
      wordFilter(u'Medi Gun', u'Medi-?Gun'),
+
    wordFilter(u'Medi Gun', u'Medi-?Gun'),
      languageBlacklist=['pl', 'de']
+
    languageBlacklist=['fr', 'de', 'pl']
  )
+
)
  
 
=== Sentry Gun ===
 
=== Sentry Gun ===
  addSafeFilter(
+
addSafeFilter(
      wordFilter(u'Sentry Gun', r"(?<!Mini.)(?<!Mah.)(?<!My.)Sentry(?![-\s]*(?:Gun|here|ahead|forward|up there|right up|, right up|'{2,}|jump)|-)"),
+
    wordFilter(u'Sentry Gun', r"(?<!Mini.)(?<!Mah.)(?<!My.)Sentry(?![-\s]*(?:Gun|here|ahead|forward|up there|right up|, right up|'{2,}|jump)|-)"),
      wordFilter(u'Sentry Guns', r'Sentry Guns', r'Sentries(?!\s+Gun)'),
+
    wordFilter(u'Sentry Guns', r'Sentry Guns', r'Sentries(?!\s+Gun)'),
      wordFilter(u'Combat Mini-Sentry Gun', r'Combat Mini-Sentry Gun', r'(?:(?:Mini|Combat)\s+)+Sentry(?!\s+Gun|-)'),
+
    wordFilter(u'Combat Mini-Sentry Gun', r'Combat Mini-Sentry Gun', r'(?:(?:Mini|Combat)\s+)+Sentry(?!\s+Gun|-)'),
      wordFilter(u'Combat Mini-Sentry Guns', r'(?:(?:Mini|Combat)[-\s]+)+Sentr(?:y[-\s]+Guns|ies)')
+
    wordFilter(u'Combat Mini-Sentry Guns', r'(?:(?:Mini|Combat)[-\s]+)+Sentr(?:y[-\s]+Guns|ies)')
  )
+
)
  addFilter(regex(r'\[\[([^][|]+\|)?(?:Sentry Guns|Sentries)\]\]', '[[$1Sentry Gun]]s')) # Put the "s" out of the link
+
addFilter(regex(r'\[\[([^][|]+\|)?(?:Sentry Guns|Sentries)\]\]', '[[$1Sentry Gun]]s')) # Put the "s" out of the link
  
 
=== Common misspellings ===
 
=== Common misspellings ===
  addSafeFilter(
+
addSafeFilter(
      wordFilter('Huntsman', 'Hunstman'),
+
    wordFilter('Huntsman', 'Hunstman'),
      wordFilter('Dispenser', 'Dis?pen[sc][eo]r'),
+
    wordFilter('Dispenser', 'Dis?pen[sc][eo]r'),
      wordFilter('Heavy', 'Hevy'),
+
    wordFilter('Heavy', 'Hevy'),
      wordFilter('Engineer', 'Enginer'),
+
    wordFilter('Engineer', 'Enginer'),
      wordFilter('Soldier', 'Solider'),
+
    wordFilter('Soldier', 'Solider'),
      wordFilter('Mini-Crit', 'Minicrit'),
+
    wordFilter('Mini-Crit', 'Minicrit'),
      wordFilter('Mini-Crits', 'Minicrits'),
+
    wordFilter('Flame Thrower', 'Flamethrower'),
      wordFilter('Chargin\' Targe', 'charg[ei][-\'n\\s]*targe?'),
+
    wordFilter('Mini-Crits', 'Minicrits'),
      wordFilter('Sasha', 'Sa[sch]+a'),
+
    wordFilter('Mini-Crit', 'Mini-crit'),
      wordFilter('Kritzkrieg', 'Kritzkreig'),
+
    wordFilter('Mini-Crits', 'Mini-crits'),
      wordFilter('screenshot', 'screen shot'),
+
    wordFilter('Critical hit', 'Critical Hit'),
      wordFilter('screenshots', 'screen shots'),
+
    wordFilter('Critical hits', 'Critical Hits'),
      wordFilter('in-game', 'ingame')
+
    wordFilter('Chargin\' Targe', 'charg[ei][-\'n\\s]*targe?'),
  )
+
    wordFilter('Kritzkrieg', 'Kritzkreig'),
  addSafeFilter(
+
    wordFilter('screenshot', 'screen shot'),
      wordFilter('Spies', 'Spys'),
+
    wordFilter('screenshots', 'screen shots'),
      languageBlacklist=['de', 'es']
+
    wordFilter('in-game', 'ingame'),
  )
+
    # wordFilter('team-colored', 'team colou?red', keepcapitalization=True)
 +
    # wordFilter('color', '(?<!Rustic )colour')
 +
)
 +
addSafeFilter(
 +
    wordFilter('Natascha', 'Natas?c?ha'),
 +
    language='en'
 +
)
 +
addSafeFilter(
 +
    wordFilter(u'tradable', u'tradeable', keepcapitalization=True),
 +
    language='en'
 +
)
 +
addSafeFilter(
 +
    wordFilter('Spies', 'Spys'),
 +
    languageBlacklist=['de', 'es']
 +
)
  
 
=== Map names ===
 
=== Map names ===
  addSafeFilter(
+
addSafeFilter(
      wordFilter('Gravel Pit', '(?<!cp_)Gravelpit', 'Gravel pit'),
+
    #wordFilter('Gravel Pit', '(?<!cp_)Gravelpit', 'Gravel pit'), - Conflicts with Gravelpit Emperor and its various translations
      wordFilter('Double Cross', 'Double Cross', '(?<!ctf_)Doublecross'),
+
    wordFilter('Badwater Basin', '(?<!pl_)Badwater Basin', '(?<!pl_)Badwater(?!\s+Basin)'),
      wordFilter('Badwater Basin', '(?<!pl_)Badwater Basin', '(?<!pl_)Badwater(?!\s+Basin)'),
+
    wordFilter('Gold Rush', '(?<!pl_)Goldrush')
      wordFilter('Gold Rush', '(?<!pl_)Goldrush')
+
)
  )
+
# Capitalisation rules for map names are whitelist-based rather than Category:Maps+blacklist based, because lots of maps are common nouns.
  # Capitalisation rules for map names are whitelist-based rather than Category:Maps+blacklist based, because lots of maps are common nouns.
+
# As such, if a map with a non-common name is added, please add it to this list.
  # As such, if a map with a non-common name is added, please add it to this list.
+
enforceCapitalization(
  enforceCapitalization(
+
    '2Fort', 'Badlands', 'Coldfront', 'Dustbowl', 'Egypt', 'Fastlane', 'Granary', 'Gullywash',
      '2Fort', 'Badlands', 'Coldfront', 'Dustbowl', 'Egypt', 'Fastlane', 'Granary', 'Gullywash',
+
    'Hightower', 'Hoodoo', 'Landfall', 'Offblast', 'Thunder Mountain',
      'Hightower', 'Hoodoo', 'Landfall', 'Offblast', 'Thunder Mountain',
+
    'Viaduct', 'Wildfire', 'Yukon'
      'Viaduct', 'Wildfire', 'Yukon'
+
)
  )
 
  
 
=== Section headers ===
 
=== Section headers ===
  addSafeFilter(
+
addSafeFilter(
      wordFilter(u'== Update history ==', u'==+ ?(?:Update history|Previous changes) ?==+')
+
    wordFilter(u'== Update history ==', u'==+ ?(?:Update history|Previous changes) ?==+'),
  )
+
    wordFilter(u'== See also ==', u'==+ ?See also ?==+'),
  enforceCapitalization('== See also ==', '== External links ==', '== Painted variants ==', '== Item set ==', '== Damage and function times ==')
+
    wordFilter(u'== External links ==', u'==+ ?External links ?==+'),
 +
    wordFilter(u'== Painted variants ==', u'==+ ?Painted variants ?==+'),
 +
    wordFilter(u'== Item set ==', u'==+ ?Item set ?==+'),
 +
    wordFilter(u'== Damage and function times ==', u'==+ ?Damage and function times ?==+'),
 +
    wordFilter(u'=== As a crafting ingredient ===', u'==+ ?As a crafting ingredient ?==+'),
 +
    wordFilter(u'== Unused content ==', u'==+ ?Unused content ?==+'),
 +
    wordFilter(u'== See also ==', u'==+ ?See also ?==+'),
 +
    wordFilter(u'== Related achievements ==', u'==+ ?Related achievements ?==+'),
 +
    wordFilter(u'== Strange variant ==', u'==+ ?Strange variant ?==+'),
 +
    wordFilter(u'=== Undocumented changes ===', u'==+ ?Undocumented changes ?==+')
 +
)
  
 
== Language-specific filters ==
 
== Language-specific filters ==
  # Language-agnostic achievements auto-translate
+
=== Language-agnostic ===
  def translateAchievements(language, pageSuffix):
+
# Language-agnostic achievements auto-translate
      try:
+
def translateAchievements(language, pageSuffix):
          tf = readLocaleFile(urllib2.urlopen(page('File:Tf_' + language + '.txt').getDownloadUrl()).read(-1))
+
    try:
      except:
+
        tf = readLocaleFile(urllib2.urlopen(page('File:Tf_' + language + '.txt').getDownloadUrl()).read(-1))
          print 'Downloading failed.'
+
    except:
          return
+
        print 'Downloading failed.'
      try:
+
        return
          languages = parseLocaleFile(tf, language=language)
+
    try:
      except:
+
        languages = parseLocaleFile(tf, language=language)
          print 'Error while parsing tf_' + language + '.txt'
+
    except:
          return
+
        print 'Error while parsing tf_' + language + '.txt'
      acceptPrefix = ['TF_SCOUT_', 'TF_SOLDIER_', 'TF_PYRO_', 'TF_DEMOMAN_', 'TF_HEAVY_', 'TF_ENGINEER_', 'TF_MEDIC_', 'TF_SNIPER_', 'TF_SPY_', 'TF_GET_', 'TF_KILL_', 'TF_BURN_', 'TF_WIN_', 'TF_PLAY_']
+
        return
      acceptSuffix = ['_NAME', '_DESC']
+
    acceptPrefix = ['TF_SCOUT_', 'TF_SOLDIER_', 'TF_PYRO_', 'TF_DEMOMAN_', 'TF_HEAVY_', 'TF_ENGINEER_', 'TF_MEDIC_', 'TF_SNIPER_', 'TF_SPY_', 'TF_GET_', 'TF_KILL_', 'TF_BURN_', 'TF_WIN_', 'TF_PLAY_']
      filteredAchievements = languagesFilter(languages, commonto=[language, 'english'], prefix=acceptPrefix, suffix=acceptSuffix, exceptions=['TF_SOLDIER_ASSIST_MEDIC_UBER_NAME'])
+
    acceptSuffix = ['_NAME', '_DESC']
      associateLocaleWordFilters(filteredAchievements, 'english', language, pageSuffix)
+
    filteredAchievements = languagesFilter(languages, commonto=[language, 'english'], prefix=acceptPrefix, suffix=acceptSuffix, exceptions=['TF_SOLDIER_ASSIST_MEDIC_UBER_NAME'])
 +
    associateLocaleWordFilters(filteredAchievements, 'english', language, pageSuffix)
 
=== Romanian filters ===
 
=== Romanian filters ===
  # Romanian characters
+
# Romanian characters
  addSafeFilter(
+
addSafeFilter(
      dumbReplaces({
+
    dumbReplaces({
          u'ş': u'ș',
+
        u'ş': u'ș',
          u'ţ': u'ț'
+
        u'ţ': u'ț'
      }), language='ro'
+
    }), language='ro'
  )
+
)
  # Romanian achievements (disabled for now)
+
# Romanian achievements (disabled for now)
  #translateAchievements('romanian', 'ro')
+
#translateAchievements('romanian', 'ro')
  # Fix User:Vulturas's stoopid:
+
# Fix User:Vulturas's stoopid:
  addSafeFilter(
+
addSafeFilter(
      wordFilter(u'batjocură', u'batjocoră'),
+
    wordFilter(u'batjocură', u'batjocoră'),
      wordFilter(u'batjocura', u'batjocora'),
+
    wordFilter(u'batjocura', u'batjocora'),
      wordFilter(u'batjocureşte', u'batjocoreşte', u'batjocorește'),
+
    wordFilter(u'batjocureşte', u'batjocoreşte', u'batjocorește'),
      wordFilter(u'batjocuri', u'batjocori'),
+
    wordFilter(u'batjocuri', u'batjocori'),
      wordFilter(u'batjocurile', u'batjocorile'),
+
    wordFilter(u'batjocurile', u'batjocorile'),
      wordFilter(u'batjocurii', u'batjocorii'),
+
    wordFilter(u'batjocurii', u'batjocorii'),
      language='ro'
+
    language='ro'
  )
+
)
  
 
=== German filters ===
 
=== German filters ===
  addSafeFilter(
+
addSafeFilter( # Requested by Picard
      wordFilter('Krit-\'n-Cola', '[CK]rit-\'?[an]\'?-Cola'),
+
    wordFilter(u'Krit-a-Cola', u'(?<!Bonk! )[CK]rit-\'?[an]\'?-Cola'),
      wordFilter('Krit', 'Crit'),
+
    wordFilter(u'Bonk! Krit-\'n-Cola', u'Bonk! [CK]rit-\'?[an]\'?-Cola'),
      wordFilter('Krits', 'Crits'),
+
    wordFilter(u'Krit', u'Crit'),
      language='de'
+
    wordFilter(u'Krits', u'Crits'),
  )
+
    wordFilter(u'Sonstiger Gegenstand', u'Diverser Gegenstand', u'diverser Gegenstand', u'sonstiger Gegenstand'),
  # German achievements (disabled for now)
+
    wordFilter(u'Sonstige Gegenstände', u'Diverse Gegenstände', 'diverse Gegenstände', 'sonstige Gegenstände'),
  #translateAchievements('german', 'de')
+
    language='de'
  addSafeFilter(
+
)
      wordFilter(u'== Update-Verlauf ==', u'==+ *(?:Update Verlauf|Letzte [Ääa]nderungen) *==+'), language='de'
+
# German achievements (disabled for now)
  )
+
#translateAchievements('german', 'de')
 +
addSafeFilter(
 +
    wordFilter(u'== Update-Verlauf ==', u'==+ *(?:Update Verlauf|Letzte [Ääa]nderungen) *==+'), language='de'
 +
)
  
 
=== Spanish filters ===
 
=== Spanish filters ===
  addSafeFilter(
+
addSafeFilter(
      wordFilter(u'Forajido', u'Gunslinger'),
+
    wordFilter(u'Forajido', u'Gunslinger'),
      wordFilter(u'Pistolón', u'Gran Masacre'),
+
    wordFilter(u'Pistolón', u'Gran Masacre'),
      wordFilter(u'Medic', u'M[eé]dico'),
+
    wordFilter(u'Medic', u'M[eé]dico(?! Medieval)'),
      wordFilter(u'Medics', u'M[eé]dicos'),
+
    wordFilter(u'Medics', u'M[eé]dicos(?! Medieval)'),
      wordFilter(u'Sniper', u'Francotirador'),
+
    wordFilter(u'Sniper', u'(?<!Rifle de )Francotirador'),
      wordFilter(u'Snipers', u'Francotiradore?s'),
+
    wordFilter(u'Snipers', u'(?<!Rifle de )Francotiradore?s'),
      wordFilter(u'Spy', u'Esp[ií]a'),
+
    wordFilter(u'Spy', u'Esp[ií]a'),
      wordFilter(u'Spies', u'Spys', u'Esp[ií]as'),
+
    wordFilter(u'Spies', u'Spys', u'Esp[ií]as'),
      wordFilter(u'Soldier', u'Soldado(?! de Fortuna)'),
+
    wordFilter(u'Soldier', u'Soldado(?! de Fortuna)'),
      wordFilter(u'Soldiers', u'Soldados'),
+
    wordFilter(u'Soldiers', u'Soldados'),
      wordFilter(u'Engineer', u'(?<!Gorra de )Ingeniero'),
+
    wordFilter(u'Engineer', u'(?<!Gorra de )Ingeniero'),
      wordFilter(u'Engineers', u'Ingenieros'),
+
    wordFilter(u'Engineers', u'Ingenieros'),
      wordFilter(u'Curiosidades', u'Trivia'),
+
    wordFilter(u'Curiosidades', u'Trivia'),
      wordFilter(u'Actualizacion Mann-Conomy', u'Mann-Conomy Update'),
+
    wordFilter(u'Actualizacion Mann-Conomy', u'Mann-Conomy Update'),
      wordFilter(u'Nivel', u'Level'),
+
    wordFilter(u'Übersaw', u'Ubersaw'),
      wordFilter(u'Cuerpo a cuerpo', u'Melee'),
+
    wordFilter(u'Artículo', u'Objeto', keepcapitalization=True),
      language='es'
+
    wordFilter(u'Artículos', u'Objetos', keepcapitalization=True),
  )
+
    language='es'
 +
)
  
  class spanishDateFilter: # Requested by [[User:BiBi|BiBi]]
+
class spanishDateFilter: # Requested by [[User:BiBi|BiBi]]
      def __init__(self):
+
    def __init__(self):
          self.reg = compileRegex(r'(?:Parche|Patch) del? (\d{1,2}) (?:del?)? (Enero|Febrero|Marzo|Abril|Mayo|Ju[nl]io|Agosto|Septiembre|Oct[ou]bre|Noviembre|Diciembre) de (\d{4})')
+
        self.reg = compileRegex(r'(?:Parche|Patch) del? (\d{1,2}) (?:del?)? (Enero|Febrero|Marzo|Abril|Mayo|Ju[nl]io|Agosto|Septiembre|Oct[ou]bre|Noviembre|Diciembre) de (\d{4})')
          self.filterName = u'Spanish date consistency filter'
+
        self.filterName = u'Spanish date consistency filter'
      def replace(self, match):
+
    def replace(self, match):
          return u'Parche del ' + u(match.group(1)) + u' de ' + u(match.group(2)).title() + u' de ' + u(match.group(3))
+
        return u'Parche del ' + u(match.group(1)) + u' de ' + u(match.group(2)).title() + u' de ' + u(match.group(3))
      def __call__(self, content, **kwargs):
+
    def __call__(self, content, **kwargs):
          return self.reg.sub(self.replace, content)
+
        return self.reg.sub(self.replace, content)
  addSafeFilter(spanishDateFilter(), language='es')
+
addSafeFilter(spanishDateFilter(), language='es')
 +
 
 +
addSafeFilter( # Requested by Dio, and later updated as requested by Ashe
 +
    wordFilter(u'== Variaciones de color ==', u'== Variaciones de Colores ==', u'==+ *Variantes +pintadas *==+'), language='es'
 +
)
 +
 
 +
addSafeFilter( # Requested by Dio
 +
    wordFilter(u'La trampa', u'El cheto', keepcapitalization=True),
 +
    wordFilter(u'Las trampas', u'Los chetos', keepcapitalization=True),
 +
    wordFilter(u'Trampas', u'Chetos', keepcapitalization=True),
 +
    wordFilter(u'Trampa', u'Cheto', keepcapitalization=True),
 +
    language='es'
 +
)
 +
 
 +
addSafeFilter( # Requested by Jagoba RL, then flipped by request of Dio
 +
    wordFilter(u'SuperActualización', u'Actualizaci[oó]n [Üüu]ber'),
 +
    language='es'
 +
)
 +
 
 +
def spanishNivelCapitalizationFilter(t, **kwargs): # Capitalize "nivel" inside <nowiki>{{item infobox}}</nowiki> and <nowiki>{{backpack item}}</nowiki> templates, requested by rZ
 +
    if t.getName().lower() == 'item infobox' and t.getParam('level'):
 +
        t.setParam('level', t.getParam('level').replace('nivel', 'Nivel'))
 +
    if t.getName().lower() == 'backpack item' and t.getParam('item-level'):
 +
        t.setParam('item-level', t.getParam('item-level').replace('nivel', 'Nivel'))
 +
    return t
 +
addTemplateFilter(spanishNivelCapitalizationFilter, language='es')
  
 
=== French filters ===
 
=== French filters ===
  enforceCapitalization(
+
enforceCapitalization(
      u'janvier', u'février', u'mars', u'avril', u'mai', u'juin', u'juillet', u'août', u'septembre', u'octobre', u'novembre', u'décembre', language='fr'
+
    u'janvier', u'février', u'mars', u'avril', u'mai', u'juin', u'juillet', u'août', u'septembre', u'octobre', u'novembre', u'décembre', language='fr'
  ) # Do not capitalize months
+
) # Do not capitalize months
  addSafeFilter(wordFilter('janvier', 'janviers'), language='fr')
+
addSafeFilter(wordFilter('janvier', 'janviers'), language='fr')
  addSafeFilter(
+
addSafeFilter(
      wordFilter(u'== Historique des mises à jour ==', u'==+ *(?:Historique des? mises? [aà] jour|Changements? pr.c.dents?) *==+'), language='fr'
+
    wordFilter(u'== Historique des mises à jour ==', u'==+ *(?:Historique des? mises? [aà] jour|Changements? pr.c.dents?) *==+'), language='fr'
  )
+
)
 +
 
 +
=== Brazilian Portuguese filters ===
 +
def portugueseInfoboxAvailabilityFilter(t, **kwargs): # Requested by Cructo
 +
    if t.getName().lower() == u'item infobox':
 +
        return t # Skip
 +
    if t.getParam('availability'):
 +
        t.setParam('availability', t.getParam('availability').replace(u'craft', u'fabricação').replace(u'Craft', u'Fabricação'))
 +
    return t
 +
addTemplateFilter(portugueseInfoboxAvailabilityFilter, language='pt-br')
 +
 
 +
addSafeFilter( # General
 +
    wordFilter(u'Fabricação', u'Crafting', keepcapitalization=True),
 +
    wordFilter(u'Conquista', u'Achievement', keepcapitalization=True),
 +
    wordFilter(u'Conquistas', u'Achievements', keepcapitalization=True),
 +
    wordFilter(u'Demomen', u'Demomans'),
 +
    wordFilter(u'Heavies', u'Heavys'),
 +
    wordFilter(u'N/D', u'N/A'),
 +
    wordFilter(u'Atualização', u'Patch', keepcapitalization=True),
 +
    wordFilter(u'Atualizações', u'Patches', keepcapitalization=True),
 +
    wordFilter(u'Desempenho', u'Performance', keepcapitalization=True),
 +
    wordFilter(u'Spies', u'Spys'),
 +
    wordFilter(u'ÜberCarga', u'Sobrecarga', u'Übercharge', u'ÜberCharge'),
 +
    wordFilter(u'Mochila', u'Backpack', u'Inventário', keepcapitalization=True),
 +
    wordFilter(u'Comunidade Steam', u'Steam Community'),
 +
    wordFilter(u'== Bugs ==', u'==+ *Defeitos *==+'),
 +
    wordFilter(u'Sala de renascimento', u'Respawn room', u'Sala de respawn', keepcapitalization=True),
 +
    wordFilter(u'Área de renascimento', u'Respawn area', u'Área de respawn',  keepcapitalization=True),
 +
    wordFilter(u'Über Atualização', u'Atualização Über', u'Atualização do Über', u'Über Update'),
 +
    wordFilter(u'Sistema de obtenção de itens', u'Sistema de drop de itens', u'Item drop system', u'Sistema de queda de itens', keepcapitalization=True),
 +
    wordFilter(u'Rajada de ar', u'Compression blast', u'Airblast', keepcapitalization=True),
 +
    wordFilter(u'minicrits', u'Mini-Crits', u'Mini-Críticos', u'Minicríticos', keepcapitalization=True),
 +
    wordFilter(u'minicrit', u'Mini-Crit', u'Mini-Crítico', u'Minicrítico', keepcapitalization=True),
 +
    wordFilter(u'Blog Oficial do TF2', u'TF2 Official Blog', u'Blog Oficial de TF2'),
 +
    wordFilter(u'Personalizada', u'Customizada', keepcapitalization=True),
 +
    wordFilter(u'Personalizado', u'Customizado', keepcapitalization=True),
 +
    wordFilter(u'Compartimento', u'Slot', keepcapitalization=True),
 +
    wordFilter(u'Jogabilidade', u'Gameplay', keepcapitalization=True),
 +
    wordFilter(u'Rei do Pedaço', u'King of the Hill'),
 +
    wordFilter(u'Disparo-alt', u'Disparo-Alt', u'Alt-Fire', keepcapitalization=True),
 +
    wordFilter(u'Disparo alternativo', u'Fogo Alternativo', u'Disparo Alternativo', keepcapitalization=True),
 +
    wordFilter(u'Botão de disparo-alt', u'Botão Direito do Mouse', keepcapitalization=True),
 +
    wordFilter(u'Habilidades', u'Abilidades', keepcapitalization=True),
 +
    wordFilter(u'Austrálio', u'Australium', keepcapitalization=True),
 +
    wordFilter(u'Cavaleiro Carente de Cavalo e Cabeça', u'Horseless Headless Horsemann'),
 +
    wordFilter(u'Festiva', u'Festive', keepcapitalization=True),
 +
    wordFilter(u'Natal Australiano', u'Australian Christmas', keepcapitalization=True),
 +
    wordFilter(u'Natal Australiano de 2011', u'Natal Australiano 2011', u'Australian Christmas 2011', keepcapitalization=True),
 +
    wordFilter(u'== Variantes pintadas ==', u'==+ *Variantes pintados *==+', u'==+ *Variações pintadas *==+', u'==+ *Variantes pintáveis *==+'),
 +
    wordFilter(u'== Curiosidades ==', u'==+ *Trivias? *==+'),
 +
    wordFilter(u'Projeto', u'Planta', keepcapitalization=True),
 +
    wordFilter(u'Projetos', u'Plantas', keepcapitalization=True),
 +
    wordFilter(u'== Histórico de atualizações ==', u'==+ *Histórico de atualização? *==+', u'==+ *Update history? *==+'),
 +
    wordFilter(u'pré-venda', u'pré-compra'),
 +
    wordFilter(u'Dia das Bruxas', u'Halloween', keepcapitalization=True),
 +
    wordFilter(u'Engine Source', u'Source Engine', u'Motor Source'),
 +
    wordFilter(u'itens', u'items', keepcapitalization=True),
 +
    wordFilter(u'pré-visualização', u'preview', u'previsão', keepcapitalization=True),
 +
    wordFilter(u'pré-visualizações', u'previews', u'previsões', keepcapitalization=True),
 +
    wordFilter(u'no Steam', u'na Steam+'),
 +
    wordFilter(u'do Steam', u'da Steam+'),
 +
    wordFilter(u'à Oficina Steam', u'para a Oficina Steam+'),
 +
    wordFilter(u'cor da equipe', u'cor do time', u'cor de time', u'cor de equipe'),
 +
    wordFilter(u'Minissentinelas', u'Minisentinelas', u'Mini-Sentinelas, keepcapitalization=True'),
 +
    wordFilter(u'Minissentinela', u'Minisentinela', u'Mini-Sentinela, keepcapitalization=True'),
 +
    wordFilter(u'Natal', u'Smissmas'),
 +
    wordFilter(u'MvM', u'MVM'),
 +
    wordFilter(u'Mann vs. Máquina', u'Mann vs Máquina', u'Mann vs. Machine', u'Mann vs Machine'),
 +
    wordFilter(u'interface', u'HUD', keepcapitalization=True),
 +
    wordFilter(u'captura de tela', u'screenshot', keepcapitalization=True),
 +
    wordFilter(u'capturas de tela', u'screenshots', keepcapitalization=True),
 +
    wordFilter(u'→', u'->', u'-->'),
 +
    wordFilter(u'←', u'<-', u'<--'),
 +
    wordFilter(u'conversa', u'bate-papo', u'bate papo', u'chat', keepcapitalization=True),
 +
    wordFilter(u'on-line', u'online', keepcapitalization=True),
 +
    wordFilter(u'off-line', u'offline', keepcapitalization=True),
 +
    wordFilter(u'tiros fatais na cabeça', u'tiros na cabeça fatais', keepcapitalization=True),
 +
    wordFilter(u'Loja Mann Co\.', u'Loja Mann Co\.\.', u'Loja da Mann Co\.'),
 +
    wordFilter(u'Peça Estranha', u'parte estranha'),
 +
    language='pt-br'
 +
)
 +
 
 +
addSafeFilter( # Quality names
 +
    wordFilter(u'Genuíno', u'Genuine', keepcapitalization=True),
 +
    wordFilter(u'Feito por Mim', u'Self-Made', keepcapitalization=True),
 +
    wordFilter(u'Normal', u'Stock', keepcapitalization=True),
 +
    wordFilter(u'Estranho', u'Strange', keepcapitalization=True),
 +
    wordFilter(u'Único', u'Unique', keepcapitalization=True),
 +
    wordFilter(u'Incomum', u'Unusual', keepcapitalization=True),
 +
    language='pt-br'
 +
)
 +
 
 +
addSafeFilter( # Weapon names
 +
    wordFilter(u'Espingarda', u'Scattergun'),
 +
    wordFilter(u'Pistola', u'Pistol'),
 +
    wordFilter(u'Taco', u'Bat'),
 +
    wordFilter(u'Lança-Foguetes', u'Rocket Launcher'),
 +
    wordFilter(u'Escopeta', u'Shotgun'),
 +
    wordFilter(u'Pá', u'Shovel'),
 +
    wordFilter(u'Lança-Chamas', u'Flame Thrower' u'Flamethrower'),
 +
    wordFilter(u'Machado de Incêndio', u'Fire Axe'),
 +
    wordFilter(u'Lança-Granadas', u'Grenade Launcher'),
 +
    wordFilter(u'Lança-Stickybombs', u'Stickybomb Launcher'),
 +
    wordFilter(u'Garrafa', u'Bottle'),
 +
    wordFilter(u'Metralhadora Giratória', u'Minigun'),
 +
    wordFilter(u'Punhos', u'Fists'),
 +
    wordFilter(u'Ferramenta de Construção', u'Build PDA'),
 +
    wordFilter(u'Ferramenta de Demolição', u'Destroy PDA'),
 +
    wordFilter(u'Arma de Seringas', u'Syringe Gun'),
 +
    wordFilter(u'Arma Médica', u'Medi Gun'),
 +
    wordFilter(u'Serra de Ossos', u'Bone Saw'),
 +
    wordFilter(u'Rifle de Precisão', u'Rifle de Sniper', u'Sniper Rifle', keepcapitalization=True),
 +
    wordFilter(u'Submetralhadora', u'SMG'),
 +
    wordFilter(u'Revólver', u'Revolver'),
 +
    wordFilter(u'Faca', u'Knife'),
 +
    wordFilter(u'Relógio de Invisibilidade', u'Invis Watch', u'Invisibility Watch'),
 +
    wordFilter(u'Kit de Disfarce', u'Disguise Kit'),
 +
    language='pt-br'
 +
)
 +
 
 +
enforceCapitalization( # Do not capitalize languages, nationalities, months or days of the week (will not include abbrevs. of weekdays, as they are also ordinal numbers)
 +
  u'inglês', u'russo', u'russa', u'francês', u'francesa', u'alemão', u'alemã', u'polonês', u'polonesa', u'brasileiro', u'brasileira', u'finlandês', u'finlandesa', u'castelhano', u'espanhol', u'espanhola', u'holandês', u'holandesa', u'chinês', u'chinesa', u'chinês simplificado', u'chinês tradicional', u'tailandês', u'tailandesa', u'ucraniano', u'ucraniana', u'árabe', u'tcheco', u'tcheca', u'dinamarquês', u'dinamarquesa', u'húngaro', u'húngara', u'italiano', u'italiana', u'japonês', u'japonesa', u'coreano', u'coreana', u'norueguês', u'norueguesa', u'português', u'portuguesa', u'romeno', u'romena', u'sueco', u'sueca', u'turco', u'egípcio', u'egípcia', u'janeiro', u'fevereiro', u'março', u'abril', u'maio', u'junho', u'julho', u'agosto', u'setembro', u'outubro', u'novembro', u'dezembro', u'segunda-feira', u'terça-feira', u'quarta-feira', u'quinta-feira', u'sexta-feira', u'sábado', u'domingo', language='pt-br'
 +
)
 +
 
 +
=== Russian filters ===
 +
addSafeFilter(
 +
    regexes({
 +
        (u'^([^<>]*)(?<!=)([""])((?:(?!\\2|[=<>]).)+)\\2([^<>]*)$', re.MULTILINE): u'$1«$3»$4',
 +
        u'«([^»]*)(?<!\')\'([^»\']+)\'(?!\')': u'«$1„$2“'
 +
    }),
 +
    language='ru'
 +
)
 +
 
 +
addSafeFilter( # Requested by FreeXMan
 +
    wordFilter(u'Зефенайя', u'Зефанайя', u'Зефинайя'),
 +
    wordFilter(u'Зефенайи', u'Зефанайи', u'Зефинайи'),
 +
    wordFilter(u'Зефенайей', u'Зефанайей', u'Зефинайей'),
 +
    wordFilter(u'Зефенай', u'Зефанай', u'Зефинай'),
 +
    wordFilter(u'Нет', u'N/A'),
 +
    wordFilter(u'Хеллоуин', u'Хэллоуин', keepcapitalization=True),
 +
    wordFilter(u'Хеллоуинский', u'Хэллоуинский', keepcapitalization=True),
 +
    wordFilter(u'Хеллоуинского', u'Хэллоуинского', keepcapitalization=True),
 +
    wordFilter(u'Хеллоуинскому', u'Хэллоуинскому', keepcapitalization=True),
 +
    wordFilter(u'Хеллоуинским', u'Хэллоуинским', keepcapitalization=True),
 +
    wordFilter(u'Хеллоуинском', u'Хэллоуинском', keepcapitalization=True),
 +
    language='ru'
 +
)
 +
 
 +
=== Korean filters ===
 +
addSafeFilter(dumbReplace(u'솔져', u'솔저'), language='ko') # Requested by Cyrus H.
 +
 
 +
=== Dutch filters ===
 +
#addSafeFilter(wordFilter(u'Niveau', u'Level', keepcapitalization=True), language='nl') # Requested by Apparition; too broad
 +
 
 +
addSafeFilter( # Requested by Warlike and Heifastus
 +
    wordFilter(u'Updateverleden', u'Updategeschiedenis', u'Update verleden', u'Update geschiedenis', keepcapitalization=True),
 +
    wordFilter(u'Galerij', u'Gallerij', keepcapitalization=True),
 +
    wordFilter(u'community', u'communitie', keepcapitalization=True),
 +
    language='nl'
 +
)
 +
 
 +
addSafeFilter( # Requested by Robin0van0der0vliet
 +
    wordFilter(u'officieel', u'offici[eë][eë]l', keepcapitalization=True),
 +
    wordFilter(u'officiële', u'offic[iï]ele', keepcapitalization=True),
 +
    language='nl'
 +
)
 +
 
 +
addSafeFilter( # Requested by Eels
 +
    wordFilter(u'voorwerpvindsysteem', u'voorwerp vind systeem', keepcapitalization=True),
 +
    wordFilter(u'Voorwerpschema', u'Voorwerp schema', keepcapitalization=True),
 +
    wordFilter(u'Voorwerpschemaupdate', u'Voorwerp schema-update', u'Voorwerpschema update', keepcapitalization=True),
 +
    language='nl'
 +
)
 +
 
 +
addSafeFilter( # Requested by GrampaSwood
 +
    regex(r'\b, en\b', ' en'),
 +
    wordFilter(u'grondvoorwerpen', u'pickups', keepcapitalization=True),
 +
    wordFilter(u'grondvoorwerp', u'pickup', keepcapitalization=True),
 +
    wordFilter(u'bijgewerkt', u'geüpdatet', keepcapitalization=True),
 +
    wordFilter(u'weetjes', u'trivia', keepcapitalization=True),
 +
    language='nl'
 +
)
 +
 
 +
=== Polish filters ===
 +
addSafeFilter( # Requested by real_alien
 +
    wordFilter(u'Exploit', u'Nadużycie', keepcapitalization=True),
 +
    wordFilter(u'Exploity', u'Nadużycia', keepcapitalization=True),
 +
    wordFilter(u'Exploitem', u'Nadużyciem', keepcapitalization=True),
 +
    wordFilter(u'Exploitu', u'Nadużycia', keepcapitalization=True),
 +
    language='pl'
 +
)
 +
 
 +
=== Hungarian filters ===
 +
addSafeFilter( # Requested by Monte
 +
    wordFilter(u'== Frissítési előzmények ==', u'==+ ?*Frissítések ?==+', u'==+ ?*Update ?==+', u'==+ ?*Javítások ?==+'),
 +
    wordFilter(u'== Lásd még ==', u'==+ ?See also ?==+'),
 +
    wordFilter(u'== Források ==', u'==+ ?References ?==+'),
 +
    wordFilter(u'== Kulisszák mögött ==', u'==+ ?Érdekességek ?==+', u'==+ ? Trivia ?==+'),
 +
    wordFilter(u'== Festett variációk ==', u'==+ ?Painted variants ?==+'),
 +
    wordFilter(u'== Sebzési és működési idők ==', u'==+ ?Damage and function times ?==+', u'==+ ?Sebzési? és [Ff]unkció idők ?==+'),
 +
    wordFilter(u'=== Mint barkácsolási kellék ===', u'==+ ?As a crafting ingredient ?==+'),
 +
    wordFilter(u'== Nem használt tartalom ==', u'==+ ?Unused content ?==+'),
 +
    wordFilter(u'== Öszefüggő teljesítmények ==', u'==+ ?Related achievements ?==+', u'==+ ?[Aa]chievements ?==+', u'==+ ?Teljesítmények ?==+'),
 +
    wordFilter(u'== Fura ritkaságú ==', u'==+ ?Strange variant ?==+'),
 +
    wordFilter(u'== Tárgy-szett ==', u'==+ ?Item set ?==+', u'==+ ?Tárgyszett ?==+', u'==+ ?Tárgy szett ?==+'),
 +
    language='hu'
 +
)
 +
 
 +
=== Swedish filters ===
 +
addSafeFilter( # Requested by BrazilianNut
 +
    wordFilter(u'Ingenjörer', u'Tekniker', keepcapitalization=True),
 +
    wordFilter(u'Ingenjören', u'Teknikern', keepcapitalization=True),
 +
    language='sv'
 +
)
  
 
== Link filters ==
 
== Link filters ==
 +
=== tf2.com to teamfortress.com ===
 +
addLinkFilter(linkDomainFilter('tf2.com', 'teamfortress.com'))
 +
 +
=== Moved links ===
 +
def movedLinks(link, **kwargs):
 +
    movedLinks = {
 +
        u'Quality#Normal': u'Normal',
 +
        u'Quality#Unique': u'Unique',
 +
        u'Quality#Vintage': u'Vintage',
 +
        u'Quality#Genuine': u'Genuine',
 +
        u'Quality#Strange': u'Strange',
 +
        u'Quality#Unusual': u'Unusual',
 +
        u'Quality#Community': u'Community (quality)',
 +
        u'Quality#Self-Made': u'Self-Made',
 +
        u'Quality#Valve': u'Valve (quality)',
 +
        u'vdc:Material': u'vdc:VMT'
 +
    }
 +
    if link.getType() == u'internal' and link.getLink() in movedLinks:
 +
        link.setLink(movedLinks[link.getLink()])
 +
    return link
 +
addLinkFilter(movedLinks)
 +
 
=== Wikipedia links filter ===
 
=== Wikipedia links filter ===
  def wikipediaLinks(link, **kwargs):
+
def wikipediaLinks(link, **kwargs):
      wikipediaRegex = compileRegex(r'^https?://(?:(\w+)\.)?wikipedia\.org/wiki/(\S+)')
+
    wikipediaRegex = compileRegex(r'^https?://(?:(\w+)\.)?wikipedia\.org/wiki/(\S+)')
      if link.getType() == u'external':
+
    if link.getType() == u'external':
          linkInfo = wikipediaRegex.search(link.getLink())
+
        linkInfo = wikipediaRegex.search(link.getLink())
          if linkInfo:
+
        if linkInfo:
              link.setType(u'internal')
+
            link.setType(u'internal')
              try:
+
            try:
                  wikiPage = urllib2.unquote(str(linkInfo.group(2))).decode('utf8', 'ignore').replace(u'_', ' ')
+
                wikiPage = urllib2.unquote(str(linkInfo.group(2))).decode('utf8', 'ignore').replace(u'_', ' ')
              except:
+
            except:
                  wikiPage = u(linkInfo.group(2)).replace(u'_', ' ')
+
                wikiPage = u(linkInfo.group(2)).replace(u'_', ' ')
              if not linkInfo.group(1) or linkInfo.group(1).lower() == u'en':
+
            if not linkInfo.group(1) or linkInfo.group(1).lower() == u'en':
                  link.setLink(u'Wikipedia:' + wikiPage) # English Wikipedia
+
                link.setLink(u'Wikipedia:' + wikiPage) # English Wikipedia
              else:
+
            else:
                  link.setLink(u'Wikipedia:' + linkInfo.group(1).lower() + u':' + wikiPage) # Non-english Wikipedia
+
                link.setLink(u'Wikipedia:' + linkInfo.group(1).lower() + u':' + wikiPage) # Non-english Wikipedia
              if link.getLabel() is None:
+
            if link.getLabel() is None:
                  link.setLabel(u'(Wikipedia)')
+
                link.setLabel(u'(Wikipedia)')
      return link
+
    return link
  addLinkFilter(wikipediaLinks)
+
addLinkFilter(wikipediaLinks)
  
=== TF2Wiki links filter ===
+
=== Fix external wiki links that should be internal links ===
  def tf2wikiLinks(link, **kwargs):
+
def fixExternalToInternalLinks(link, **kwargs):
      tf2wikiRegex1 = compileRegex(r'^https?://[-.\w]*tf2wiki\.net/wiki/(\S+)$')
+
    wikiExternalRe = compileRegex(r'^https?://wiki\.(teamfortress|tf2)\.com/wiki/(\S+)$')
      tf2wikiRegex2 = compileRegex(r'^https?://[-.\w]*tf2wiki\.net/w[-_/\w]+?/([^/\s]+)$')
+
    if link.getType() == 'external':
      if link.getType() == 'external':
+
        linkMatch = wikiExternalRe.search(link.getLink())
          linkInfo = tf2wikiRegex1.search(link.getLink())
+
        if linkMatch:
          isMedia = False
+
            link.setType('internal')
          if not linkInfo:
+
            try:
              linkInfo = tf2wikiRegex2.search(link.getLink())
+
                wikiPage = u(urllib2.unquote(str(linkMatch.group(2))).decode('utf8', 'ignore').replace(u'_', ' '))
              isMedia = True
+
            except:
          if linkInfo:
+
                wikiPage = u(linkMatch.group(2)).replace(u'_', ' ')
              link.setType('internal')
+
            link.setLink(wikiPage)
              try:
+
            if link.getLabel() is None:
                  wikiPage = u(urllib2.unquote(str(linkInfo.group(1))).decode('utf8', 'ignore').replace(u'_', ' '))
+
                link.setLabel(wikiPage)
              except:
+
    return link
                  wikiPage = u(linkInfo.group(1)).replace(u'_', ' ')
+
addLinkFilter(fixExternalToInternalLinks)
              label = wikiPage
 
              if isMedia:
 
                  if wikiPage[-4:].lower() == '.wav':
 
                      wikiPage = 'Media:' + wikiPage
 
                  else:
 
                      wikiPage = ':File:' + wikiPage
 
              link.setLink(wikiPage)
 
              if link.getLabel() is None:
 
                  link.setLabel(label)
 
      return link
 
  addLinkFilter(tf2wikiLinks)
 
  
 
=== Category removal on pages using {{tl|Item infobox}} ===
 
=== Category removal on pages using {{tl|Item infobox}} ===
  def removeCategory(l, **kwargs):
+
def removeCategory(l, **kwargs):
      catsToRemove = [u'Category:Weapons', u'Category:Hats', u'Category:Primary weapons', u'Category:Secondary weapons', u'Category:Melee weapons', u'Category:PDA1 weapons', u'Category:PDA2 weapons', u'Category:Miscellaneous items', u'Category:Tools', u'Category:Action items', u'Category:Taunts', u'Community-contributed items']
+
    catsToRemove = [u'Category:Weapons', u'Category:Hats', u'Category:Primary weapons', u'Category:Secondary weapons', u'Category:Melee weapons', u'Category:PDA1 weapons', u'Category:PDA2 weapons', u'Category:Miscellaneous items', u'Category:Tools', u'Category:Action items', u'Category:Taunts', u'Community-contributed items']
      regLang = compileRegex('/[^/]+$')
+
    regLang = compileRegex('/[^/]+$')
      if 'article' not in kwargs or regLang.sub(u'', l.getLink()) not in catsToRemove:
+
    if 'article' not in kwargs or regLang.sub(u'', l.getLink()) not in catsToRemove:
          return l
+
        return l
      if u'Category:Item infobox usage' not in kwargs['article'].getCategories():
+
    if u'Category:Item infobox usage' not in kwargs['article'].getCategories():
          return l
+
        return l
      return None
+
    return None
  addLinkFilter(removeCategory)
+
addLinkFilter(removeCategory)
  
=== FPSBanana to GameBanana ===
+
=== Remove trailing slashes from internal links ===
  addLinkFilter(linkDomainFilter('fpsbanana.com', 'gamebanana.com'))
+
def removeTrailingSlash(l, **kwargs):
 +
    if l.getType() != u'internal' or not len(l.getLink()):
 +
        return l
 +
    if l.getLink()[-1] == '/':
 +
        l.setLink(l.getLink()[:-1])
 +
    return l
 +
addLinkFilter(removeTrailingSlash)
  
=== Remove trailing slashes from internal links ===
+
=== Convert [[:Category:Patches|patch]] links to {{tl|Patch name}} ===
  def removeTrailingSlash(l, **kwargs):
+
def patchNameLinkFilter(l, **kwargs):
      if l.getType() != u'internal':
+
    if l.getType() != u'internal':
          return l
+
        return l
      if l.getLink()[-1] == '/':
+
    regPatchName = compileRegex(u'(January|February|March|April|May|June|July|August|September|October|November|December)\\s+(\\d+),\\s+(\\d{4,})\\s+Patch(?:/\\w+)?')
          l.setLink(l.getLink()[:-1])
+
    result = regPatchName.match(l.getLink())
      return l
+
    if result is None or l.getLabel().find(result.group(2)) == -1 or l.getLabel().find(result.group(3)) == -1:
  addLinkFilter(removeTrailingSlash)
+
        return l
 +
    monthNames = ('january', 'february', 'march', 'april', 'may', 'june', 'july', 'august', 'september', 'october', 'november', 'december')
 +
    patchType = u''
 +
    if l.getLink().lower().find(u'beta') != -1:
 +
        patchType = u'|beta'
 +
    elif l.getLink().lower().find(u'xbox') != -1:
 +
        patchType = u'|xbox'
 +
    elif l.getLink().lower().find(u'classic') != -1:
 +
        patchType = u'|classic'
 +
    return template(u'<nowiki>{{Patch name|' + u(monthNames.index(result.group(1).lower()) + 1) + u'|' + u(result.group(2)) +  u'|' + u(result.group(3)) + patchType + u'}}</nowiki>')
 +
addLinkFilter(patchNameLinkFilter)
  
 
== Template filters ==
 
== Template filters ==
 
=== Template renaming ===
 
=== Template renaming ===
  def templateRenameMapping(t, **kwargs):
+
def templateRenameMapping(t, **kwargs):
      templateMap = {
+
    templateMap = {
          # Format goes like this (without the "#" in front obviously):
+
        # Format goes like this (without the "#" in front obviously):
          #'Good template name': ['Bad template lowercase name 1', 'Bad template lowercase name 2', 'Bad template lowercase name 3'],
+
        #'Good template name': ['Bad template lowercase name 1', 'Bad template lowercase name 2', 'Bad template lowercase name 3'],
          # Last line has no comma at the end
+
        # Last line has no comma at the end
          'Scout Nav': ['scout nav/ro'],
+
        'Scout Nav': ['scout nav/ro'],
          'Soldier Nav': ['soldier nav/ro'],
+
        'Soldier Nav': ['soldier nav/ro'],
          'Pyro Nav': ['pyro nav/ro'],
+
        'Pyro Nav': ['pyro nav/ro'],
          'Heavy Nav': ['heavy nav/ro'],
+
        'Heavy Nav': ['heavy nav/ro'],
          'Demoman Nav': ['demoman nav/ro'],
+
        'Demoman Nav': ['demoman nav/ro'],
          'Engineer Nav': ['engineer nav/ro'],
+
        'Engineer Nav': ['engineer nav/ro'],
          'Medic Nav': ['medic nav/ro'],
+
        'Medic Nav': ['medic nav/ro'],
          'Sniper Nav': ['sniper nav/ro'],
+
        'Sniper Nav': ['sniper nav/ro'],
          'Spy Nav': ['spy nav/ro'],
+
        'Spy Nav': ['spy nav/ro'],
          'Promo nav': ['mncnav', 'pokernightnav', 'l4dnav', 'kfnav']
+
        'Promo Nav': ['mncnav', 'pokernightnav', 'l4dnav', 'kfnav'],
      }
+
        'Crush': ['pngcrush'],
      for n in templateMap:
+
        'Class infobox': ['infobox class'],
          if t.getName().lower() in templateMap[n]:
+
        'Video infobox': ['infobox video'],
              t.setName(n)
+
        'Comic infobox': ['infobox comics']
      return t
+
    }
  addTemplateFilter(templateRenameMapping)
+
    for n in templateMap:
 +
        if t.getName().lower() in templateMap[n]:
 +
            t.setName(n)
 +
    return t
 +
addTemplateFilter(templateRenameMapping)
 +
 
 +
=== Lang template renaming ===
 +
def langsTemplateRenameMapping(t, **kwargs):
 +
    templateMap = {'Scout Nav': 'scout nav',
 +
                    'Soldier Nav': 'soldier nav',
 +
                    'Pyro Nav': 'pyro nav',
 +
                    'Demoman Nav': 'demoman nav',
 +
                    'Heavy Nav': 'heavy nav',
 +
                    'Engineer Nav': 'engineer nav',
 +
                    'Medic Nav': 'medic nav',
 +
                    'Sniper Nav': 'sniper nav',
 +
                    'Spy Nav': 'spy nav',
 +
                    'Audio Nav': 'audionav',
 +
                    'Video Nav': 'videonav',
 +
                    'Class Strategy Nav': 'class strategy'
 +
                    }
 +
    for template in templateMap:
 +
        langTemplateRe = compileRegex(templateMap[template] + '/(ar|cs|da|de|es|fi|fr|hu|it|ja|ko|nl|no|pl|pt|pt-br|ro|ru|sv|tr|zh-hans|zh-hant)')
 +
        if langTemplateRe.match(t.getName().lower()):
 +
            t.setName(template)
 +
    return t
 +
addTemplateFilter(langsTemplateRenameMapping)
 +
 
 +
=== Reindent all infoboxes ===
 +
 
 +
def infoboxIndentFilter(t, **kwargs):
 +
    itemInfoboxes = ('item infobox', 'map infobox', 'item set infobox', 'mission infobox', 'class infobox', 'hazard infobox', 'pickup infobox', 'video infobox', 'comic infobox', 'website infobox', 'company infobox')
 +
    tName = t.getName().lower()
 +
    if 'infobox' in tName and tName not in itemInfoboxes:
 +
        t.indentationMatters(True)
 +
        t.setDefaultIndentation(2)
 +
    return t
 +
addTemplateFilter(infoboxIndentFilter, lowPriority=True)
  
 
=== Manage all {{tl|Item infobox}}es ===
 
=== Manage all {{tl|Item infobox}}es ===
  def infoboxFilter(t, **kwargs):
+
def infoboxFilter(t, **kwargs):
      filteredTemplates = ('weapon infobox', 'hat infobox', 'tool infobox', 'item infobox') # Only do stuff to these templates
+
    filteredTemplates = ('weapon infobox', 'hat infobox', 'tool infobox', 'item infobox') # Only do stuff to these templates
      if t.getName().lower() not in filteredTemplates:
+
    if t.getName().lower() not in filteredTemplates:
          return t # Skip
+
        return t # Skip
      t.setName('Item infobox')
+
    t.setName('Item infobox')
      t.indentationMatters(True) # Reindents every time, not only when modifying values
+
    t.indentationMatters(True) # Reindents every time, not only when modifying values
      paramAliases = { # Parameter alias 'goodParam': 'badParam', or 'goodParam': [list of bad params].
+
    paramAliases = { # Parameter alias 'goodParam': 'badParam', or 'goodParam': [list of bad params].
          'name': ['weapon-name-override', 'hat-name-override', 'tool-name-override', 'NAME', 'name-override'],
+
        'name': ['weapon-name-override', 'hat-name-override', 'tool-name-override', 'NAME', 'name-override'],
          'image': ['weapon-image', 'hat-image', 'tool-image'],
+
        'image': ['weapon-image', 'hat-image', 'tool-image'],
          'kill-text-1': 'kill-text',
+
        'kill-text-1': 'kill-text',
          'team-colors': 'has-team-colors',
+
        'team-colors': 'has-team-colors',
          'two-models': 'has-two-models',
+
        'two-models': 'has-two-models',
          'slot': ['weapon-slot', 'hat-slot', 'tool-slot'],
+
        'slot': ['weapon-slot', 'hat-slot', 'tool-slot'],
          'trade': 'tradable',
+
        'trade': 'tradable',
          'gift': 'giftable',
+
        'gift': 'giftable',
          'craft': 'craftable',
+
        'craft': 'craftable',
          'paint': ['paintable', 'Paint'],
+
        'paint': ['paintable', 'Paint'],
          'rename': ['name-tag', 'nametag'],
+
        'rename': ['name-tag', 'nametag'],
          'loadout': 'display-loadout-stats',
+
        'loadout': 'display-loadout-stats',
          'level': 'level-and-type'
+
        'level': 'level-and-type',
      }
+
        'loadout-prefix': 'hide-loadout-prefix'
      catstoCheck = { # Mapping 'templateAttribute': [List of 'Category|templateAttributeValue']
+
    }
          'type': ['Taunts|taunt', 'Action items|action', 'Hats|hat', 'Miscellaneous items|misc item', 'Tools|tools', 'Weapons|weapon'],
+
    catstoCheck = { # Mapping 'templateAttribute': [List of 'Category|templateAttributeValue']
          'slot': ['Primary weapons|primary', 'Secondary weapons|secondary', 'Melee weapons|melee', 'PDA1 weapons|pda 1', 'PDA2 weapons|pda 2']
+
        'type': ['Taunts|taunt', 'Action items|action', 'Taunts|action taunt', 'Hats|hat', 'Miscellaneous items|misc item', 'Tools|tools', 'Weapons|weapon'],
      }
+
        'slot': ['Primary weapons|primary', 'Secondary weapons|secondary', 'Melee weapons|melee', 'PDA1 weapons|pda 1', 'PDA2 weapons|pda 2']
      catsDontCheck = [u'Category:Beta and unused content', 'Category:Taunts'] # Type and slot won't be modified on these pages
+
    }
      preferedOrder = [ # Prefered order of keys inside template
+
    catsDontCheck = [u'Category:Beta and unused content', 'Category:Taunts'] # Type and slot won't be modified on these pages
          'name', 'game', 'type', 'beta', 'unused', 'image', 'imagewidth', 'team-colors', 'two-models', 'skin-image-red', 'skin-image-blu', 'TFC-model', 'QTF-model', 'kill-icon-#', 'kill-text-#', 'used-by', 'slot', 'crafting-slot', 'custom-slot', 'contributed-by', 'released', 'availability', 'trade', 'gift', 'craft', 'paint', 'rename', 'medieval', 'ammo-loaded', 'ammo-carried', 'ammo-type', 'show-ammo', 'reload', 'loadout', 'loadout-prefix', 'quality', 'level', '%ATTRIBUTES%', 'item-description', 'item-uses', 'item-flags', 'item-expiration'
+
    preferedOrder = [ # Prefered order of keys inside template
      ]
+
        'name', 'game', 'type', 'beta', 'unused', 'image', 'imagewidth', '3d-team', '3d-alt', '3d-team-alt', '3d-image-#', '3d-button-#', '3d-viewname-#', 'number-of-3d-images', 'number-of-3d-team-images', 'number-of-3d-alt-images', 'view#', 'view#name', 'team-colors', 'team-colors-class#', 'team-colors-class#-name', 'team-colors-width', 'two-models', 'skin-image-red', 'skin-image-blu', 'tfc-model', 'tfc-model-3d-image-#', 'tfc-model-3d-viewname-#', 'tfc-model-3d-button-#', 'qtf-model', 'hide-kill-icon', 'kill-icon-#', 'kill-text-#', 'kill-tooltip-#', 'used-by', 'slot', 'crafting-slot', 'custom-slot', 'equip-region', 'equip-region-#', 'weapon-script', 'contributed-by', 'released', 'released-major', 'availability', 'trade', 'gift', 'marketable', 'craft', 'paint', 'rename', 'numbered', 'medieval', 'ammo-loaded', 'ammo-carried', 'ammo-type', 'show-ammo', 'reload', 'loadout', 'loadout-prefix', 'prefix', 'suffix', 'quality', '%ATTRIBUTES%', 'item-kind', 'item-level', 'level', 'paint-color', 'decal-icon', 'unusual-icon', 'stat-icon', 'strange-icon', 'pyroland-icon', 'halloween-icon', 'limited', 'unusual-effect', 'grade', 'wear', 'item-description', 'item-uses', 'item-flags', 'item-expiration'
      checkEnglish = ['trade', 'craft', 'paint', 'rename'] # If these attributes aren't yes or no, check the values on the english page
+
    ]
      attributeTypes = ['neutral', 'positive', 'negative'] # Possible loadout attribute types
+
    checkEnglish = ['trade', 'gift', 'marketable', 'craft', 'paint', 'rename', 'numbered', 'medieval', 'contributed-by', 'equip-region', 'equip-region-#'] # If these attributes aren't yes or no, check the values on the english page
      regLang = compileRegex('/[^/]+$')
+
    attributeTypes = ['neutral', 'positive', 'negative'] # Possible loadout attribute types
      # Step 0 - Check categories:
+
    regLang = compileRegex('/[^/]+$')
      tCats = None
+
    # Step 0 - Check categories:
      isTFC = False # Assume false by default
+
    tCats = None
      if 'article' in kwargs:
+
    isTFC = False # Assume false by default
          if kwargs['article'] is not None:
+
    if 'article' in kwargs:
              cats2 = kwargs['article'].getCategories()
+
        if kwargs['article'] is not None:
              tCats = []
+
            cats2 = kwargs['article'].getCategories()
              for c in cats2:
+
            tCats = []
                  tCats.append(u(regLang.sub(u'', u(c))))
+
            for c in cats2:
              isTFC = u'Category:Weapons (Classic)' in tCats
+
                tCats.append(u(regLang.sub(<nowiki>u''</nowiki>, u(c))))
      # Step 1 - Rename obsolete attributes
+
            isTFC = u'Category:Weapons (Classic)' in tCats
      for p in paramAliases:
+
    # Step 1 - Rename obsolete attributes
          if type(paramAliases[p]) is type([]):
+
    for p in paramAliases:
              for a in paramAliases[p]:
+
        if type(paramAliases[p]) is type([]):
                  t.renameParam(a, p)
+
            for a in paramAliases[p]:
          else:
+
                t.renameParam(a, p)
              t.renameParam(paramAliases[p], p)
+
        else:
      # Step 2 - Fix ammo stuff
+
            t.renameParam(paramAliases[p], p)
      if t.getParam('ammo-carried') == u'1' or t.getParam('ammo-loaded') == u'1':
+
    # Step 2 - Fix ammo stuff, delete pricing attributes
          t.delParam('ammo-loaded', 'ammo-carried', 'ammo-type')
+
    t.delParam('ammo', 'price', 'show-price', 'purchasable', 'backpack-image')
          t.setParam('show-ammo', 'off')
+
    # Step 3 - Fix reload stuff
      # Step 3 - Fix more ammo stuff, delete pricing attributes
+
    if t.getParam('reload') is None:
      t.delParam('ammo', 'price', 'show-price', 'purchasable', 'backpack-image')
+
        t.renameParam('reload-type', 'reload')
      # Step 4 - Fix reload stuff
+
    # Step 4 - Count, split, order and fix loadout attributes
      if t.getParam('reload') is None:
+
    attrNumber = 1
          t.renameParam('reload-type', 'reload')
+
    regexAttrSplit = compileRegex(r'\s*<br[^<>]*>\s*')
      # Step 5 - Count, split, order and fix loadout attributes
+
    for a in attributeTypes:
      attrNumber = 1
+
        if t.getParam(a + '-attributes') is not None:
      regexAttrSplit = compileRegex(r'\s*<br[^<>]*>\s*')
+
            attrs = regexAttrSplit.split(t.getParam(a + '-attributes'))
      for a in attributeTypes:
+
            for attr in attrs:
          if t.getParam(a + '-attributes') is not None:
+
                t.setParam('att-' + str(attrNumber) + '-' + a, attr)
              attrs = regexAttrSplit.split(t.getParam(a + '-attributes'))
+
                attrNumber += 1
              for attr in attrs:
+
            t.delParam(a + '-attributes')
                  t.setParam('att-' + str(attrNumber) + '-' + a, attr)
+
    if tCats is not None:
                  attrNumber += 1
+
        # Step 5 - Lookup english fallback on certain attributes
              t.delParam(a + '-attributes')
+
        fetchEnglish = False
      if tCats is not None:
+
        values = {}
          # Step 6 - Lookup english fallback on certain attributes
+
        for attr in checkEnglish:
          fetchEnglish = False
+
            if t.getParam(attr) is not None and t.getParam(attr).lower() not in (u'yes', u'no'):
          values = {}
+
                fetchEnglish = True
          for attr in checkEnglish:
+
            elif t.getParam(attr) is not None:
              if t.getParam(attr) is not None and t.getParam(attr).lower() not in (u'yes', u'no'):
+
                values[attr] = t.getParam(attr).lower()
                  fetchEnglish = True
+
        if regLang.search(kwargs['article'].title):
              elif t.getParam(attr) is not None:
+
            englishArticle = page(regLang.sub(<nowiki>u''</nowiki>, kwargs['article'].title))
                  values[attr] = t.getParam(attr).lower()
+
            try:
          if regLang.search(kwargs['article'].title):
+
                englishContent = englishArticle.getWikiText()
              englishArticle = page(regLang.sub('', kwargs['article'].title))
+
            except:
              try:
+
                englishContent = u''
                  englishContent = englishArticle.getWikiText()
+
            englishContent, englishTemplates, englishKeys = templateExtract(englishContent)
              except:
+
            for englishT in englishTemplates.values():
                  englishContent = u''
+
                if englishT.getName().lower() in filteredTemplates:
              englishContent, englishTemplates = templateExtract(englishContent)
+
                    for p in paramAliases:
              for englishT in englishTemplates:
+
                        if type(paramAliases[p]) is type([]):
                  if englishT.getName().lower() in filteredTemplates:
+
                            for a in paramAliases[p]:
                      for p in paramAliases:
+
                                englishT.renameParam(a, p)
                          if type(paramAliases[p]) is type([]):
+
                        else:
                              for a in paramAliases[p]:
+
                            englishT.renameParam(paramAliases[p], p)
                                  englishT.renameParam(a, p)
+
                    for attr in checkEnglish:
                          else:
+
                        if englishT.getParam(attr) is not None:
                              englishT.renameParam(paramAliases[p], p)
+
                            values[attr] = templateRestore(englishT.getParam(attr), englishTemplates, englishKeys)
                      for attr in checkEnglish:
+
                    break
                          if englishT.getParam(attr) is not None and englishT.getParam(attr).lower() in (u'yes', u'no'):
+
        for attr in values:
                              values[attr] = englishT.getParam(attr).lower()
+
            t.setParam(attr, values[attr])
                      break
+
        checkCats = True
          for attr in values:
+
        for c in catsDontCheck:
              t.setParam(attr, values[attr])
+
            if c in tCats:
          checkCats = True
+
                checkCats = False
          for c in catsDontCheck:
+
                break
              if c in tCats:
+
        if not isTFC and not t.getParam('custom-slot') and checkCats:
                  checkCats = False
+
            # Step 6 - Set certains attributes based on page categories
                  break
+
            for cname in catstoCheck:
          if not isTFC and not t.getParam('custom-slot') and checkCats:
+
                cname = u(cname)
              # Step 7 - Set certains attributes based on page categories
+
                found = None
              for cname in catstoCheck:
+
                foundmultiple = False
                  cname = u(cname)
+
                for c in catstoCheck[cname]:
                  found = None
+
                    cat, val = u(c).split(u'|')
                  foundmultiple = False
+
                    cat = u'Category:' + u(regLang.sub(<nowiki>u''</nowiki>, cat))
                  for c in catstoCheck[cname]:
+
                    if cat in tCats:
                      cat, val = u(c).split(u'|')
+
                        if found is not None:
                      cat = u'Category:' + u(regLang.sub(u'', cat))
+
                            foundmultiple = True
                      if cat in tCats:
+
                        found = val
                          if found is not None:
+
                if not foundmultiple:
                              foundmultiple = True
+
                    pass#t.setParam(cname, found) # Temporarily disabled for Made Man
                          found = val
+
            if u'Category:Weapons' not in tCats:
                  if not foundmultiple:
+
                t.delParam('slot')
                      t.setParam(cname, found)
+
            # Step 7 - Convert neutral attributes - Disabled
              if u'Category:Weapons' not in tCats:
+
            """desc = []
                  t.delParam('slot')
+
            if t.getParam('item-description') is not None:
              # Step 8 - Convert neutral attributes - Disabled
+
                desc = regexAttrSplit.split(t.getParam('item-description'))
              """desc = []
+
            for i in range(1, 10):
              if t.getParam('item-description') is not None:
+
                if t.getParam('att-' + str(i) + '-neutral') is not None:
                  desc = regexAttrSplit.split(t.getParam('item-description'))
+
                    desc.append(t.getParam('att-' + str(i) + '-neutral'))
              for i in range(1, 10):
+
                    t.delParam('att-' + str(i) + '-neutral')
                  if t.getParam('att-' + str(i) + '-neutral') is not None:
+
            desc2 = []
                      desc.append(t.getParam('att-' + str(i) + '-neutral'))
+
            for d in desc:
                      t.delParam('att-' + str(i) + '-neutral')
+
                if d.strip():
              desc2 = []
+
                    desc2.append(d.strip())
              for d in desc:
+
            if len(desc2):
                  if d.strip():
+
                t.setParam('item-description', u'<br />'.join(desc2))"""
                      desc2.append(d.strip())
+
    # Step 7.5 TEMPORARY: Add numbered = no
              if len(desc2):
+
    if not isTFC and not t.getParam('numbered'):
                  t.setParam('item-description', u'<br />'.join(desc2))"""
+
        t.setParam('numbered', 'no')
          #  
+
    # Step 8 - Do TFC stuff
          # Commenting out because of new parameter 'beta'
+
    if isTFC:
          # and there being no way to distinguish between beta items & unused content thus far
+
        t.setParam('type', 'weapon')
          # seb26 04:58, 12 February 2011 (UTC)
+
        t.setParam('game', 'tfc')
          #
+
    # Step 9 - Set correct preferred indentation
          # if u'Category:Beta and unused content' in tCats:
+
    for k in ['quality', 'item-level', 'level', 'item-description', 'item-uses', 'item-flags', 'level-and-type', 'loadout-name', 'loadout-prefix', 'hide-loadout-prefix', 'limited', 'grade', 'wear', 'unusual-effect', 'item-kind', 'prefix', 'suffix', 'decal-icon', 'unusual-icon', 'stat-icon', 'strange-icon', 'pyroland-icon', 'halloween-icon', 'custom-icon', 'killcount', 'rankson', 'rankson2', 'rankson3', 'rankson4', 'rankson5', 'rankson6', 'rankson7', 'rankson8', 'rankson9', 'item-expiration']:
          #    t.setParam('unused', 'yes')
+
        t.setPreferedIndentation(k, 2)
      # Step 9 - Do TFC stuff
+
    for i in range(1, 10):
      if isTFC:
+
        for a in attributeTypes:
          t.setParam('type', 'weapon')
+
            t.setPreferedIndentation('att-' + str(i) + '-' + a, 2)
          t.setParam('game', 'tfc')
+
    # Step 10 - Build correct attribute order
      # Step 10 - Set correct preferred indentation
+
    newOrder = []
      for k in ['quality', 'level', 'item-description', 'item-uses', 'item-flags', 'level-and-type', 'loadout-name', 'loadout-prefix']:
+
    for o in preferedOrder:
          t.setPreferedIndentation(k, 2)
+
        if o.find('#') == -1:
      for i in range(1, 10):
+
            newOrder.append(o)
          for a in attributeTypes:
+
            continue
              t.setPreferedIndentation('att-' + str(i) + '-' + a, 2)
+
        if o == '%ATTRIBUTES%':
      # Step 11 - Build correct attribute order
+
            for i in range(1, 10):
      newOrder = []
+
                for a in attributeTypes:
      for o in preferedOrder:
+
                    newOrder.append('att-' + str(i) + '-' + a)
          if o.find('#') == -1:
+
        for i in range(1, 30):
              newOrder.append(o)
+
            newOrder.append(o.replace('#', str(i)))
              continue
+
    t.setPreferedOrder(newOrder)
          if o == '%ATTRIBUTES%':
+
    # Step 11 - replace deprecated hat/misc types
              for i in range(1, 10):
+
    if t.getParam('type') is not None and t.getParam('type').lower() in (u'hat', u'hats', u'head', u'headwear', u'misc item', u'misc.', u'misc', u'miscellaneous', u'miscellaneous item'):
                  for a in attributeTypes:
+
        t.setParam('type', 'cosmetic')
                      newOrder.append('att-' + str(i) + '-' + a)
+
    # Step 12 - There is no step 12
          for i in range(1, 10):
+
    return t
              newOrder.append(o.replace('#', str(i)))
+
addTemplateFilter(infoboxFilter)
      t.setPreferedOrder(newOrder)
 
      # Step 12 - There is no step 12
 
      return t
 
  addTemplateFilter(infoboxFilter)
 
  
 
=== Remove useless templates ===
 
=== Remove useless templates ===
  def removeUselessTemplate(t, **kwargs):
+
def removeUselessTemplate(t, **kwargs):
      if t.getName().lower() in (u'targeted', u'languages'):
+
    if t.getName().lower() in (u'targeted', u'languages', u'auto lang cat'):
          return None # Delete template
+
        return None # Delete template
      return t
+
    return t
  addTemplateFilter(removeUselessTemplate)
+
addTemplateFilter(removeUselessTemplate)
 +
 
 +
=== Replace Wikipedia link template with interwiki link ===
 +
def replaceWikipediaTemplate(t, **kwargs):
 +
    if 'article' not in kwargs:
 +
        return t
 +
    if t.getName().lower() != 'w':
 +
        return t
 +
    link = u'[[w:'
 +
    if t.getParam('lang'):
 +
        link += t.getParam('lang') + u':'
 +
    link += t.getParam('1') + u'|'
 +
    if t.getParam('2'):
 +
        link += t.getParam('2')
 +
    else:
 +
        link += t.getParam('1')
 +
    link += u']]'
 +
    return link
 +
addTemplateFilter(replaceWikipediaTemplate, lowPriority=True)
  
 
=== Remove manual video IDs from {{tl|Weapon Demonstration}} ===
 
=== Remove manual video IDs from {{tl|Weapon Demonstration}} ===
  def weaponDemonstrationFilter(t, **kwargs):
+
def weaponDemonstrationFilter(t, **kwargs):
      if t.getName().lower() != 'weapon demonstration':
+
    if t.getName().lower() != 'weapon demonstration':
          return t # Skip
+
        return t # Skip
      t.delParam('1')
+
    t.delParam('1')
      return t
+
    return t
  addTemplateFilter(weaponDemonstrationFilter)
+
addTemplateFilter(weaponDemonstrationFilter)
  
 
=== Filter parameters of certain templates ===
 
=== Filter parameters of certain templates ===
  def templateParamFilter(t, **kwargs):
+
def templateParamFilter(t, **kwargs):
      params = { # Map: 'lowercase template name': ['list', 'of', 'params', 'to', 'filter']
+
    params = { # Map: 'lowercase template name': ['list', 'of', 'params', 'to', 'filter']
          'patch layout': ['before', 'after', 'current'],
+
        'patch layout': ['before', 'after', 'current'],
          'item infobox': ['released']
+
        'item infobox': ['released'],
      }
+
        'update history': [1],
      if t.getName().lower() not in params:
+
        'otherwikis': [1, 2]
          return t
+
    }
      for p in params[t.getName().lower()]:
+
    if t.getName().lower() not in params:
          if t.getParam(p):
+
        return t
              t.setParam(p, fixContent(t.getParam(p), **kwargs))
+
    for p in params[t.getName().lower()]:
      return t
+
        if t.getParam(p):
  addTemplateFilter(templateParamFilter)
+
            t.setParam(p, fixContent(t.getParam(p), **kwargs))
 +
    return t
 +
addTemplateFilter(templateParamFilter)
  
 
=== Remove obsolete parameters ===
 
=== Remove obsolete parameters ===
  def obsoleteParameterFilter(t, **kwargs):
+
def obsoleteParameterFilter(t, **kwargs):
      params = { # Map: 'lowercase template name': ['list', 'of', 'params', 'to', 'delete']
+
    params = { # Map: 'lowercase template name': ['list', 'of', 'params', 'to', 'delete']
          'blueprint': ['ingredient-#n-local', 'result-local'],
+
        'blueprint': ['ingredient-#n-local', 'result-local', 'result-#n-local'],
          'taunt': ['weapon-#n-local'],
+
        'taunt': ['weapon-#n-local'],
      }
+
        'patch layout': ['diff-#n'],
      if t.getName().lower() not in params:
+
        'item infobox': ['crafting-slot'],
          return t
+
        'model info': ['location'],
      for p in params[t.getName().lower()]:
+
        'map infobox': ['map-stamp-link']
          p = u(p)
+
    }
          if p.find(u'#n') != -1:
+
    if t.getName().lower() not in params:
              for i in range(10):
+
        return t
                  t.delParam(p.replace(u'#n', str(i)))
+
    for p in params[t.getName().lower()]:
          else:
+
        p = u(p)
              t.delParam(p)
+
        if p.find(u'#n') != -1:
      return t
+
            for i in range(10):
  addTemplateFilter(obsoleteParameterFilter)
+
                t.delParam(p.replace(u'#n', str(i)))
 
+
        else:
=== Fix [[Template:Map infobox|map infobox]] attribute ===
+
            t.delParam(p)
  def mapTypeFix(t, **kwargs):
+
    return t
      mapTypeDict = {
+
addTemplateFilter(obsoleteParameterFilter, lowPriority=True)
          'Arena': ['Badlands (Arena)', 'Granary (Arena)', 'Hardhat', 'Lumberyard', 'Nucleus', 'Offblast', 'Ravine', 'Sawmill', 'Watchtower', 'Well (Arena)'],
 
          'Capture the Flag': ['2Fort', 'Atrophy (Capture the Flag)', 'Bedrock', 'Cloudburst', 'Converge', 'Convoy', 'Deliverance', 'Double Cross', 'Fusion', 'HAARP', 'Landfall', 'Mercy', 'Overlook', 'Premuda', 'Sawmill (Capture the Flag)', 'Slate (Capture the Flag)', 'Stockpile', 'Turbine', 'Vector', 'Well (Capture the Flag)', 'Wildfire', 'Wiretap'],
 
          'Control Point': ['5Gorge', 'Badlands', 'Coldfront', 'DeGroot Keep', 'Dustbowl', 'Egypt', 'Fastlane', 'Freight', 'Furnace Creek', 'Glacier', 'Gorge', 'Granary', 'Gravel Pit', 'Gullywash', 'Junction', 'Mann Manor', 'Mountain Lab', 'Obscure', 'Steel', 'Well', 'Yukon'],
 
          'King of the Hill': ['Harvest', 'Harvest Event', 'Lakeside', 'Moonshine', 'Nucleus (King of the Hill)', 'Sawmill (King of the Hill)', 'Viaduct'],
 
          'Payload': ['Badwater Basin', 'Cashworks', 'Cranetop', 'Frontier', 'Gold Rush', 'Hoodoo', 'Swiftwater', 'Thunder Mountain', 'Upward', 'Waste'],
 
          'Payload Race': ['Animus', 'Cornfield', 'Hightower', 'Highwind', 'Nightfall', 'Panic', 'Pipeline', 'Scoville', 'Solitude'],
 
          'Territorial Control': ['Hydro', 'Meridian'],
 
          'Training mode': ['Dustbowl (Training)', 'Target', 'Walkway']
 
          }
 
      if 'article' not in kwargs:
 
          return t
 
      basetitle = u(kwargs['article'].title.split('/')[0])
 
      if t.getName().lower() != 'map infobox':
 
          return t
 
      for n in mapTypeDict:
 
          if basetitle in mapTypeDict[n]:
 
              t.setParam('game-type', n)
 
      return t
 
  addTemplateFilter(mapTypeFix)
 
  
=== Implement [[Template:Backpack Item Link|Backpack Item Link template]] ===
+
=== Add <code>day</code>/<code>month</code>/<code>year</code> to {{tl|Patch layout}} ===
  def backpackLink(t, **kwargs):
+
def patchLayoutFilter(t, **kwargs):
      if t.getName().lower() != 'item infobox':
+
    if t.getName().lower() != 'patch layout' or 'article' not in kwargs:
          return t
+
        return t
      if t.getParam('contributed-by') is None:
+
    t.setPreferedOrder(['game', 'before', 'day', 'month', 'year', 'after', 'source-title', 'source', 'source-lang'] + [['source-' + str(n) + '-title', 'source-' + str(n), 'source-' + str(n) + '-lang'] for n in xrange(10)] + ['updatelink', 'update', 'update-link', 'update-lang', 'hide-diff'] + [['diff-' + str(n)] for n in xrange(10)] + ['notes'])
          return t
+
    t.delParam('current')
      contributor = t.getParam('contributed-by')
+
    regPatchName = compileRegex(u'^(January|February|March|April|May|June|July|August|September|October|November|December)\\s+(\\d+),\\s+(\\d{4,})\\s+Patch(?:/\\w+)?')
      optf2LinkReg = compileRegex(<nowiki>r'^\[http://optf2.com/item/(\S+)\s(.[^\[\]]+)]$'</nowiki>)
+
    result = regPatchName.match(u(kwargs['article'].title))
      result = optf2LinkReg.search(contributor)
+
    if result is not None:
      if result:
+
        t.setParam('day', result.group(2))
          t.setParam('contributed-by', <nowiki>'{{Backpack Item Link|' + result.group(1) + '|' + result.group(2) + '}}'</nowiki>)
+
        t.setParam('month', result.group(1).lower())
      return t
+
        t.setParam('year', result.group(3))
  addTemplateFilter(backpackLink)
+
    return t
 +
addTemplateFilter(patchLayoutFilter)
  
 
=== Implement {{tl|Dictionary}} ===
 
=== Implement {{tl|Dictionary}} ===
  class DictionaryUpdater:
+
class DictionaryUpdater:
      def __init__(self):
+
    def __init__(self):
          self.subpageTemplateLang = <nowiki>"""{{#switch:{{{lang|{{SUBPAGENAME}}}}}|%options%}}<noinclude><hr style="margin: 1em 0em;" /><div style="font-size: 95%;">\n:[[File:Pictogram info.png|15px|text-top|link=]]&nbsp;'''Note''': Any changes made here will be automatically overwritten by a bot. Please ''do not'' make changes here as they will be lost. Edit '''[[:%dictionary%|the master page]]''' instead.\n:%missing%</div>[[Category:Template dictionary|%dictionaryname%/%keyname%]]</noinclude>"""</nowiki>
+
        self.subpageTemplateLang = <nowiki>"""{{#switch:{{{lang|{{SUBPAGENAME}}}}}|%options%}}"""</nowiki>
          self.subpageTemplateParam = <nowiki>"""{{#switch:{{{1|}}}|%options%}}<noinclude><hr style="margin: 1em 0em;" /><div style="font-size: 95%;">\n:[[File:Pictogram info.png|15px|text-top|link=]]&nbsp;'''Note''': Any changes made here will be automatically overwritten by a bot. Please ''do not'' make changes here as they will be lost. Edit '''[[:%dictionary%|the master page]]''' instead.</div>[[Category:Template dictionary|%dictionaryname%/%keyname%]]</noinclude>"""</nowiki>
+
        self.subpageTemplateParam = <nowiki>"""{{#switch:{{{1|}}}|%options%}}"""</nowiki>
          self.invalidParamError = <nowiki>"""<div style="font-size: 95%; color: #CC0000;">\n:[[File:Pictogram info.png|15px|text-top|link=]]&nbsp;'''Error''': Invalid parameter passed.</div>"""</nowiki>
+
        self.invalidParamError = <nowiki>"""<span class="error">Error: invalid param.</span>[[Category:ERROR]]"""</nowiki>
          self.subpageTemplateID = <nowiki>"""%string%<noinclude><hr style="margin: 1em 0em;" /><div style="font-size: 95%;">\n:[[File:Pictogram info.png|15px|text-top|link=]]&nbsp;'''Note''': Any changes made here will be automatically overwritten by a bot. Please ''do not'' make changes here as they will be lost. Edit '''[[:%dictionary%|the master page]]''' instead.</div>[[Category:Template dictionary|%dictionaryname%/%keyname%]]</noinclude>"""</nowiki>
+
        self.invalidKeyNameCharacters = <nowiki>"""#<>[]{}"""</nowiki>
          self.dictionaries = {
+
        self.subpageTemplateID = <nowiki>"""%string%"""</nowiki>
              u'Template:Dictionary/items': { # Dictionary page
+
        self.partialUpdateThreshold = 750 # Update SyncData every n edits
                  'name': 'items', # Dictionary name (used for categorizing)
+
        self.dictionaries = {
                  'sync': 'Template:Dictionary/items/Special:SyncData' # Page holding last sync data
+
            u'Template:Dictionary/items': { # Dictionary page
              },
+
                'name': 'items', # Dictionary name (used for categorizing)
              u'Template:Dictionary/common strings': { # Warning: no underscore
+
                'sync': 'Template:Dictionary/items/Special:SyncData' # Page holding last sync data
                  'name': 'common strings',
+
            },
                  'sync': 'Template:Dictionary/common strings/Special:SyncData'
+
            u'Template:Dictionary/common strings': { # Warning: no underscore
              },
+
                'name': 'common strings',
              u'Template:Dictionary/classes': {
+
                'sync': 'Template:Dictionary/common strings/Special:SyncData'
                  'name': 'classes',
+
            },
                  'sync': 'Template:Dictionary/classes/Special:SyncData'
+
            u'Template:Dictionary/classes': {
              },
+
                'name': 'classes',
              u'Template:Dictionary/demonstrations': {
+
                'sync': 'Template:Dictionary/classes/Special:SyncData'
                  'name': 'demonstrations',
+
            },
                  'sync': 'Template:Dictionary/demonstrations/Special:SyncData'
+
            u'Template:Dictionary/demonstrations': {
              },
+
                'name': 'demonstrations',
              u'Template:Dictionary/price': {
+
                'sync': 'Template:Dictionary/demonstrations/Special:SyncData'
                  'name': 'price',
+
            },
                  'sync': 'Template:Dictionary/price/Special:SyncData'
+
            u'Template:Dictionary/price': {
              }
+
                'name': 'price',
          }
+
                'sync': 'Template:Dictionary/price/Special:SyncData',
          self.subpageSeparator = u'/'
+
                'allTemplate': <nowiki>'{{{{{template|item price/fmt}}}|%options%|tt={{{tt|yes}}}}}'</nowiki>
          # List of supported languages, in prefered order
+
            },
          self.languages = [u'en', u'ar', u'cs', u'da', u'de', u'es', u'fi', u'fr', u'hu', u'it', u'ja', u'ko', u'nl', u'no', u'pl', u'pt', u'pt-br', u'ro', u'ru', u'sv', u'zh-hans', u'zh-hant']
+
            u'Template:Dictionary/merchandise': {
          self.defaultLang = u'en'
+
                'name': 'merchandise',
          self.filterName = u'Your friendly neighborhood dictionary updater'
+
                'sync': 'Template:Dictionary/merchandise/Special:SyncData'
          self.commentsExtract = compileRegex(r'<!--([\S\s]+?)-->')
+
            },
          self.stringsExtract = compileRegex(r'(?:^[ \t]*#[ \t]*([^\r\n]*?)[ \t]*$\s*)?^[ \t]*([^\r\n]+?[ \t]*(?:\|[ \t]*[^\r\n]+?[ \t]*)*):([ \t]*[^\r\n]+?[ \t]*$|\s*[\r\n]+(?:\s*[-\w]+[ \t]*:[ \t]*[^\r\n]+[ \t]*$)+)', re.IGNORECASE | re.MULTILINE)
+
            u'Template:Dictionary/decorated': {
          self.translationExtract = compileRegex(r'^[ \t]*([-\w]+)[ \t]*:[ \t]*([^\r\n]+)[ \t]*$', re.IGNORECASE | re.MULTILINE)
+
                'name': 'decorated',
          addWhitelistPage(self.dictionaries.keys())
+
                'sync': 'Template:Dictionary/decorated/Special:SyncData'
      def generateSubpage(self, keyName, data, currentDict, syncData):
+
            },
          h = hashlib.md5()
+
            u'Template:Dictionary/descriptions': {
          if type(data) is type({}): # Subkeys (translations or not)
+
                'name': 'descriptions',
              isTranslation = True
+
                'sync': 'Template:Dictionary/descriptions/Special:SyncData'
              subpage = u(self.subpageTemplateLang)
+
            },
              for k in data:
+
            u'Template:Dictionary/dyk': {
                  if k not in self.languages:
+
                'name': 'dyk',
                      isTranslation = False
+
                'sync': 'Template:Dictionary/dyk/Special:SyncData'
                      subpage = u(self.subpageTemplateParam)
+
            },
                      break
+
            u'Template:Dictionary/tournament medals': {
              ordered = []
+
                'name': 'tournament medals',
              if isTranslation:
+
                'sync': 'Template:Dictionary/tournament medals/Special:SyncData'
                  missing = []
+
            },
                  for lang in self.languages:
+
            u'Template:Dictionary/attributes': {
                      if lang in data:
+
                'name': 'attributes',
                          ordered.append(lang + u'=' + data[lang])
+
                'sync': 'Template:Dictionary/attributes/Special:SyncData'
                          h.update((lang + u'=' + data[lang]).encode('utf8'))
+
            },
                      else:
+
            u'Template:Dictionary/steam ids': {
                          missing.append(lang)
+
                'name': 'steam ids',
                          h.update((u'null-' + lang).encode('utf8'))
+
                'sync': 'Template:Dictionary/steam ids/Special:SyncData'
                  if self.defaultLang in data:
+
            },
                      ordered.append(u'#default=' + data[self.defaultLang])
+
            u'Template:Dictionary/achievements/scout': {
                  if len(missing):
+
                'name': 'achievements/scout',
                      subpage = subpage.replace(u'%missing%', <nowiki>u"'''Languages missing''': "</nowiki> + u', '.join(missing))
+
                'sync': 'Template:Dictionary/achievements/scout/Special:SyncData'
                  else:
+
            },
                      subpage = subpage.replace(u'%missing%', <nowiki>u"'''Supported languages''': All"</nowiki>)
+
            u'Template:Dictionary/achievements/soldier': {
              else: # Not a translation
+
                'name': 'achievements/soldier',
                  h.update('Any-')
+
                'sync': 'Template:Dictionary/achievements/soldier/Special:SyncData'
                  subkeys = data.keys()
+
            },
                  subkeys.sort()
+
            u'Template:Dictionary/achievements/pyro': {
                  for k in subkeys:
+
                'name': 'achievements/pyro',
                      ordered.append(k + u'=' + data[k])
+
                'sync': 'Template:Dictionary/achievements/pyro/Special:SyncData'
                      h.update((k + u'=' + data[k]).encode('utf8'))
+
            },
                  #ordered.append(u'#default=' + u(self.invalidParamError))
+
            u'Template:Dictionary/achievements/demoman': {
              subpage = subpage.replace(u'%options%', u'|'.join(ordered))
+
                'name': 'achievements/demoman',
          else: # No subkeys
+
                'sync': 'Template:Dictionary/achievements/demoman/Special:SyncData'
              data = u(data)
+
            },
              subpage = self.subpageTemplateID
+
            u'Template:Dictionary/achievements/heavy': {
              h.update(u(u'ID-' + data).encode('utf8'))
+
                'name': 'achievements/heavy',
              subpage = subpage.replace(u'%string%', data)
+
                'sync': 'Template:Dictionary/achievements/heavy/Special:SyncData'
          h = u(h.hexdigest())
+
            },
          if keyName in syncData and syncData[keyName] == h:
+
            u'Template:Dictionary/achievements/engineer': {
              return # Same hash
+
                'name': 'achievements/engineer',
          syncData[keyName] = h # Update sync data
+
                'sync': 'Template:Dictionary/achievements/engineer/Special:SyncData'
          subpage = subpage.replace(u'%dictionary%', currentDict)
+
            },
          subpage = subpage.replace(u'%dictionaryname%', self.dictionaries[currentDict]['name'])
+
            u'Template:Dictionary/achievements/medic': {
          subpage = subpage.replace(u'%keyname%', keyName)
+
                'name': 'achievements/medic',
          editPage(currentDict + self.subpageSeparator + keyName, subpage, summary=<nowiki>u'Pushed changes from [[:' + currentDict + u']] for string "' + keyName + u'".'</nowiki>, minor=True, nocreate=False)
+
                'sync': 'Template:Dictionary/achievements/medic/Special:SyncData'
      def processComment(self, commentString, currentDict, definedStrings, syncData):
+
            },
          commentContents = []
+
            u'Template:Dictionary/achievements/sniper': {
          for extractedStr in self.stringsExtract.finditer(commentString):
+
                'name': 'achievements/sniper',
              comment = u''
+
                'sync': 'Template:Dictionary/achievements/sniper/Special:SyncData'
              if extractedStr.group(1):
+
            },
                  comment = u'# ' + u(extractedStr.group(1)) + u'\n'
+
            u'Template:Dictionary/achievements/spy': {
              dataString = u(extractedStr.group(3))
+
                'name': 'achievements/spy',
              if dataString.find(u'\r') == -1 and dataString.find(u'\n') == -1: # Assume no subkeys
+
                'sync': 'Template:Dictionary/achievements/spy/Special:SyncData'
                  data = dataString.strip()
+
            },
                  dataWriteback = u' ' + data
+
            u'Template:Dictionary/achievements/general': {
              else: # There's subkeys; detect whether this is a translation or not
+
                'name': 'achievements/general',
                  data = {}
+
                'sync': 'Template:Dictionary/achievements/general/Special:SyncData'
                  isTranslation = True
+
            },
                  for translation in self.translationExtract.finditer(dataString.strip()):
+
            u'Template:Dictionary/achievements/halloween': {
                      data[u(translation.group(1))] = u(translation.group(2))
+
                'name': 'achievements/halloween',
                      if u(translation.group(1)) not in self.languages:
+
                'sync': 'Template:Dictionary/achievements/halloween/Special:SyncData'
                          isTranslation = False
+
            },
                  ordered = []
+
            u'Template:Dictionary/achievements/treasure hunt': {
                  if isTranslation:
+
                'name': 'achievements/treasure hunt',
                      for lang in self.languages:
+
                'sync': 'Template:Dictionary/achievements/treasure hunt/Special:SyncData'
                          if lang in data:
+
            },
                              ordered.append(u' ' + lang + u': ' + data[lang])
+
            u'Template:Dictionary/achievements/replay': {
                  else: # Not a translation, so order in alphabetical order
+
                'name': 'achievements/replay',
                      subkeys = data.keys()
+
                'sync': 'Template:Dictionary/achievements/replay/Special:SyncData'
                      subkeys.sort()
+
            },
                      for subk in subkeys:
+
            u'Template:Dictionary/achievements/summer camp': {
                          ordered.append(u' ' + subk + u': ' + data[subk])
+
                'name': 'achievements/summer camp',
                  dataWriteback = u'\n' + u'\n'.join(ordered)
+
                'sync': 'Template:Dictionary/achievements/summer camp/Special:SyncData'
              keyNames = u(extractedStr.group(2)).lower().split(u'|')
+
            },
              validKeyNames = []
+
            u'Template:Dictionary/achievements/foundry': {
              for keyName in keyNames:
+
                'name': 'achievements/foundry',
                  keyName = keyName.replace(u'_', u' ').strip()
+
                'sync': 'Template:Dictionary/achievements/foundry/Special:SyncData'
                  if keyName in definedStrings:
+
            },
                      continue # Duplicate key
+
            u'Template:Dictionary/achievements/christmas': {
                  definedStrings.append(keyName)
+
                'name': 'achievements/christmas',
                  validKeyNames.append(keyName)
+
                'sync': 'Template:Dictionary/achievements/christmas/Special:SyncData'
                  self.generateSubpage(keyName, data, currentDict, syncData)
+
            },
              commentContents.append(comment + u' | '.join(validKeyNames) + u':' + dataWriteback)
+
            u'Template:Dictionary/achievements/astro-chievements': {
          return u'\n\n'.join(commentContents)
+
                'name': 'achievements/astro-chievements',
      def __call__(self, content, **kwargs):
+
                'sync': 'Template:Dictionary/achievements/astro-chievements/Special:SyncData'
          if 'article' not in kwargs:
+
            },
              return content
+
            u'Template:Dictionary/achievements/mann vs. machievements': {
          if u(kwargs['article'].title) not in self.dictionaries:
+
                'name': 'achievements/mann vs. machievements',
              return content
+
                'sync': 'Template:Dictionary/achievements/mann vs. machievements/Special:SyncData'
          currentDict = u(kwargs['article'].title)
+
            },
          syncPage = page(self.dictionaries[currentDict]['sync'])
+
            u'Template:Dictionary/achievements/standin': {
          try:
+
                'name': 'achievements/standin',
              syncDataText = u(syncPage.getWikiText()).split(u'\n')
+
                'sync': 'Template:Dictionary/achievements/standin/Special:SyncData'
          except: # Page probably doesn't exist
+
            },
              syncDataText = u''
+
            u'Template:Dictionary/achievements/process': {
          syncData = {}
+
                'name': 'achievements/process',
          for sync in syncDataText:
+
                'sync': 'Template:Dictionary/achievements/process/Special:SyncData'
              sync = u(sync.strip())
+
            },
              if not sync:
+
            u'Template:Dictionary/achievements/snakewater': {
                  continue
+
                'name': 'achievements/snakewater',
              sync = sync.split(u':', 2)
+
                'sync': 'Template:Dictionary/achievements/snakewater/Special:SyncData'
              if len(sync) == 2:
+
            },
                  syncData[sync[0]] = sync[1]
+
            u'Template:Dictionary/achievements/powerhouse': {
          oldSyncData = syncData.copy()
+
                'name': 'achievements/powerhouse',
          newContent = u''
+
                'sync': 'Template:Dictionary/achievements/powerhouse/Special:SyncData'
          previousIndex = 0
+
            },
          definedStrings = []
+
            u'Template:Dictionary/achievements/other games': {
          for comment in self.commentsExtract.finditer(content):
+
                'name': 'achievements/other games',
              newContent += content[previousIndex:comment.start()]
+
                'sync': 'Template:Dictionary/achievements/other games/Special:SyncData'
              previousIndex = comment.end()
+
            },
              # Process current comment
+
            u'Template:Dictionary/achievements/pass time': {
              newContent += u'<!--\n\n' + self.processComment(u(comment.group(1)).strip(), currentDict, definedStrings, syncData) + u'\n\n-->'
+
                'name': 'achievements/pass time',
          newContent += content[previousIndex:]
+
                'sync': 'Template:Dictionary/achievements/pass time/Special:SyncData'
          # Check if we need to update sync data
+
            },
          needUpdate = False
+
            u'Template:Dictionary/blueprints': {
          for k in syncData:
+
                'name': 'blueprints',
              if k not in oldSyncData or oldSyncData[k] != syncData[k]:
+
                'sync': 'Template:Dictionary/blueprints/Special:SyncData'
                  needUpdate = True
+
            },
                  break
+
            u'Template:Dictionary/defindex': {
          # Check for deleted strings
+
                'name': 'defindex',
          for k in oldSyncData:
+
                'sync': 'Template:Dictionary/defindex/Special:SyncData'
              if k not in definedStrings:
+
            },
                  try:
+
            u'Template:Dictionary/quad': {
                      deletePage(currentDict + self.subpageSeparator + k, 'Removed deleted string "' + k + u'" from [[:' + currentDict + u']].')
+
                'name': 'quad',
                  except:
+
                'sync': 'Template:Dictionary/quad/Special:SyncData',
                      pass
+
                'blankString': '-'
                  if k in syncData:
+
            },
                      del syncData[k]
+
            u'Template:Dictionary/gameinfo': {
                  needUpdate = True
+
                'name': 'gameinfo',
          if needUpdate:
+
                'sync': 'Template:Dictionary/gameinfo/Special:SyncData'
              # Build syncdata string representation
+
            }
              syncKeys = syncData.keys()
+
        }
              syncKeys.sort()
+
        self.subpageSeparator = u'/'
              syncLines = []
+
        # List of supported languages, in prefered order
              for k in syncKeys:
+
        self.languages = [u'en', u'ar', u'bg', u'cs', u'da', u'de', u'es', u'fi', u'fr', u'hu', u'it', u'ja', u'ko', u'nl', u'no', u'pl', u'pt', u'pt-br', u'ro', u'ru', u'sv', u'th', u'tr', u'uk', u'vi', u'zh-hans', u'zh-hant']
                  syncLines.append(k + u':' + syncData[k])
+
        self.defaultLang = u'en'
              editPage(syncPage, u'\n'.join(syncLines), summary=<nowiki>u'Updated synchronization information for [[:' + currentDict + u']].'</nowiki>, minor=True, nocreate=False)
+
        self.allKeyName = u'_all_'
          return newContent
+
        self.filterName = u'Your friendly neighborhood dictionary updater'
  addFilter(DictionaryUpdater())
+
        self.commentsExtract = compileRegex(r'<!--([\S\s]+?)-->')
 +
        self.subkeyName = compileRegex(r'^([-\w]+)$', re.IGNORECASE)
 +
        addWhitelistPage(self.dictionaries.keys())
 +
        self.editCounts = {}
 +
    def updateSyncData(self, currentDict, syncData, note=''):
 +
        # Build syncdata string representation
 +
        syncKeys = syncData.keys()
 +
        syncKeys.sort()
 +
        syncLines = []
 +
        for k in syncKeys:
 +
            syncLines.append(k + u':' + syncData[k])
 +
        if note:
 +
            note = u' (' + u(note) + u')'
 +
        editPage(self.dictionaries[currentDict]['sync'], u'\n'.join(syncLines), summary=<nowiki>u'Updated synchronization information for [[:' + currentDict + u']]' + note + u'.'</nowiki>, minor=True, nocreate=False)
 +
    def generateSubpage(self, keyName, data, currentDict, syncData):
 +
        h = hashlib.md5()
 +
        if type(data) is type({}): # Subkeys (translations or not)
 +
            isTranslation = True
 +
            subpage = u(self.subpageTemplateLang)
 +
            for k in data:
 +
                if 'blankString' in self.dictionaries[currentDict] and data[k] == self.dictionaries[currentDict]['blankString']:
 +
                    data[k] = u''
 +
                if isTranslation and k not in self.languages:
 +
                    isTranslation = False
 +
                    subpage = u(self.subpageTemplateParam)
 +
            ordered = []
 +
            unordered = {}
 +
            if isTranslation:
 +
                missing = []
 +
                for lang in self.languages:
 +
                    if lang in data:
 +
                        ordered.append(lang + u'=' + data[lang])
 +
                        unordered[lang] = data[lang]
 +
                        h.update((lang + u'=' + data[lang]).encode('utf8'))
 +
                    else:
 +
                        missing.append(lang)
 +
                        h.update((u'null-' + lang).encode('utf8'))
 +
                if self.defaultLang in data:
 +
                    ordered.insert(0, u'#default=' + data[self.defaultLang])
 +
                if len(missing):
 +
                    subpage = subpage.replace(u'%missing%', <nowiki>u"Languages missing: "</nowiki> + u', '.join(missing))
 +
                else:
 +
                    subpage = subpage.replace(u'%missing%', <nowiki>u"Supported languages: all"</nowiki>)
 +
            else: # Not a translation
 +
                h.update('Any-')
 +
                subkeys = data.keys()
 +
                subkeys.sort()
 +
                for k in subkeys:
 +
                    ordered.append(k + u'=' + data[k])
 +
                    unordered[k] = data[k]
 +
                    h.update((k + u'=' + data[k]).encode('utf8'))
 +
            if 'allTemplate' in self.dictionaries[currentDict] and (len(unordered) or len(self.dictionaries[currentDict]['allTemplate']['params'])):
 +
                allKey = []
 +
                keys = unordered.keys()
 +
                keys.sort()
 +
                for k in keys:
 +
                    allKey.append(k + u'=' + unordered[k])
 +
                insertIndex = 0
 +
                if isTranslation and self.defaultLang in data:
 +
                    insertIndex = 1
 +
                ordered.insert(insertIndex, u(self.allKeyName) + u'=' + u(self.dictionaries[currentDict]['allTemplate'].replace(u'%options%', u'|'.join(allKey))))
 +
            subpage = subpage.replace(u'%options%', u'|'.join(ordered))
 +
        else: # No subkeys
 +
            data = u(data)
 +
            subpage = self.subpageTemplateID
 +
            h.update(u(u'ID-' + data).encode('utf8'))
 +
            subpage = subpage.replace(u'%string%', data)
 +
        h = u(h.hexdigest())
 +
        if keyName in syncData and syncData[keyName] == h:
 +
            return # Same hash
 +
        subpage = subpage.replace(u'%dictionary%', currentDict)
 +
        subpage = subpage.replace(u'%dictionaryname%', self.dictionaries[currentDict]['name'])
 +
        subpage = subpage.replace(u'%keyname%', keyName)
 +
        if editPage(currentDict + self.subpageSeparator + keyName, subpage, summary=<nowiki>u'Pushed changes from [[:' + currentDict + u']] for string "' + keyName + u'".'</nowiki>, minor=True, nocreate=False):
 +
            syncData[keyName] = h # Update sync data
 +
            if currentDict not in self.editCounts:
 +
                self.editCounts[currentDict] = 0
 +
            self.editCounts[currentDict] += 1
 +
            if self.editCounts[currentDict] > self.partialUpdateThreshold:
 +
                self.editCounts[currentDict] = 0
 +
                self.updateSyncData(currentDict, syncData, 'Partial update')
 +
    def dedup(self, l):
 +
        s = set()
 +
        for i in l:
 +
            if i not in s:
 +
                s.add(i)
 +
                yield i
 +
    def processComment(self, commentString, currentDict, definedStrings, syncData):
 +
        commentContents = []
 +
        commentString = u(commentString).replace(u'\r', u'')
 +
        parseState = {
 +
            'currentKeys': [],
 +
            'currentSubkeys': {},
 +
            'currentKeyIsValid': False,
 +
            'abortCurrentSubkeys': False,
 +
            'subKeyLines': {},
 +
        }
 +
        subPageData = {}
 +
        def finalize():
 +
            if parseState['currentKeyIsValid']:
 +
                # End processing of current set of subkeys.
 +
                if not parseState['abortCurrentSubkeys']:
 +
                    isTranslation = True
 +
                    for k in parseState['currentKeys']:
 +
                        assert k not in subPageData, 'Internal logic consistency error: duplicate key %r' % (k,)
 +
                        subPageData[k] = {}
 +
                        for subKey, data in parseState['currentSubkeys'].items():
 +
                            isTranslation = isTranslation and subKey in self.languages
 +
                            subPageData[k][subKey] = data
 +
                    if isTranslation:
 +
                        for lang in self.languages:
 +
                            if lang in parseState['subKeyLines']:
 +
                                commentContents.append(parseState['subKeyLines'][lang])
 +
                    else:
 +
                        for subKey in sorted(parseState['subKeyLines'].keys()):
 +
                            commentContents.append(parseState['subKeyLines'][subKey])
 +
                    parseState['abortCurrentSubkeys'] = False
 +
                parseState['currentKeyIsValid'] = False
 +
        for line in commentString.split(u'\n'):
 +
            if u'WINDBOT_INVALID' in line:
 +
                commentContents.append(line)
 +
                continue
 +
            def badLine(reason):
 +
                if parseState['abortCurrentSubkeys']:
 +
                    for k in sorted(parseState['subKeyLines'].keys()):
 +
                        commentContents.append(parseState['subKeyLines'][k] + u'  // WINDBOT_INVALID Other subkeys for this key are invalid')
 +
                    parseState['subKeyLines'] = {}
 +
                if parseState['currentKeyIsValid'] and not parseState['abortCurrentSubkeys']:
 +
                    parseState['abortCurrentSubkeys'] = True
 +
                else:
 +
                    parseState['currentKeyIsValid'] = False
 +
                commentContents.append(line + u'  // WINDBOT_INVALID ' + u(reason.replace(u':', ' ')))
 +
            if line.strip() == u'':
 +
                finalize()
 +
                commentContents.append(line)
 +
                continue
 +
            if line.strip()[0] == u'#':  # Human comment
 +
                commentContents.append(line)
 +
                continue
 +
            if line[0] not in (u' ', '\t'):  # Key, or key + no-subkey data
 +
                if line.find(u':') == -1:  # Colon was probably forgotten
 +
                    badLine('Maybe a forgotten colon?')
 +
                    continue
 +
                if parseState['currentKeyIsValid']:
 +
                    badLine('Missing linebreak before new key?')
 +
                    continue
 +
                beforeColon, afterColon = line.split(u':', 1)
 +
                afterColon = afterColon.strip()
 +
                # Check keys.
 +
                keyNames = [k.replace(u'_', u' ').replace(u'#', u'').strip().lower() for k in beforeColon.strip().split(u'|')]
 +
                keyNames = [k for k in self.dedup(keyNames) if k]
 +
                if len(keyNames) == 0:
 +
                    badLine('No valid key names')
 +
                    continue
 +
                duplicateKey = None
 +
                badKey = None
 +
                for k in keyNames:
 +
                    for c in self.invalidKeyNameCharacters:
 +
                        if c in k:
 +
                            badKey = k
 +
                            break
 +
                    if k in definedStrings:
 +
                        duplicateKey = k
 +
                        break
 +
                    if k in subPageData:
 +
                        duplicateKey = k
 +
                        break
 +
                if duplicateKey:
 +
                    badLine('Duplicate key: %r' % duplicateKey)
 +
                    continue
 +
                if badKey:
 +
                    badLine('Key has invalid characters: %r' % badKey)
 +
                    continue
 +
                # Key looks good.
 +
                for k in keyNames:
 +
                    definedStrings.add(k)
 +
                # Check for no-subkey data.
 +
                if afterColon:
 +
                    for k in keyNames:
 +
                        subPageData[k] = afterColon
 +
                    commentContents.append(u' | '.join(keyNames) + u': ' + afterColon)
 +
                else:
 +
                    parseState['currentKeyIsValid'] = True
 +
                    parseState['abortCurrentSubkeys'] = False
 +
                    parseState['currentKeys'] = keyNames
 +
                    parseState['currentSubkeys'] = {}
 +
                    parseState['subKeyLines'] = {}
 +
                    commentContents.append(u' | '.join(keyNames) + u':')
 +
                continue
 +
            # Sub-key definition follows (has a space as first character).
 +
            if parseState['abortCurrentSubkeys']:
 +
                commentContents.append(line)
 +
                continue
 +
            if not parseState['currentKeyIsValid']:
 +
                badLine('Sub-key being defined despite no valid key')
 +
                continue
 +
            if line.find(u':') == -1:  # Colon was probably forgotten
 +
                badLine('Missing colon in subkey definition')
 +
                continue
 +
            beforeColon, afterColon = line.strip().split(u':', 1)
 +
            subKeyNames = [k.strip().lower() for k in beforeColon.strip().split(u'|')]
 +
            subKeyNames = [k for k in self.dedup(subKeyNames) if k]
 +
            badSubkeyName = None
 +
            duplicateSubkey = None
 +
            for k in subKeyNames:
 +
                if not self.subkeyName.match(k):
 +
                    badSubkeyName = k
 +
                    break
 +
                if k in parseState['currentSubkeys'].keys():
 +
                    duplicateSubkey = k
 +
                    break
 +
            if badSubkeyName:
 +
                badLine('Invalid subkey name: %r' % badSubkeyName)
 +
                continue
 +
            if duplicateSubkey:
 +
                badLine('Duplicate subkey name: %r' % duplicateSubkey)
 +
                continue
 +
            subKeyData = afterColon.strip()
 +
            if not subKeyData:
 +
                badLine('Empty data')
 +
                continue
 +
            for k in subKeyNames:
 +
                parseState['currentSubkeys'][k] = subKeyData
 +
                parseState['subKeyLines'][k] = u'  ' + k + u': ' + subKeyData
 +
        finalize()
 +
        for k, data in subPageData.items():
 +
            self.generateSubpage(k, data, currentDict, syncData)
 +
        return u'\n'.join(commentContents)
 +
    def __call__(self, content, **kwargs):
 +
        if 'article' not in kwargs:
 +
            return content
 +
        if u(kwargs['article'].title) not in self.dictionaries:
 +
            return content
 +
        currentDict = u(kwargs['article'].title)
 +
        if random.randint(0, 50) == 0: # With probability 2%, ignore syncdata completely. Helps with stale syncdata and people overwriting things.
 +
            syncDataText = u''
 +
        else:
 +
            try:
 +
                syncDataText = u(page(self.dictionaries[currentDict]['sync']).getWikiText()).split(u'\n')
 +
            except: # Page probably doesn't exist
 +
                syncDataText = u''
 +
        syncData = {}
 +
        for sync in syncDataText:
 +
            sync = u(sync.strip())
 +
            if not sync:
 +
                continue
 +
            sync = sync.split(u':', 1)
 +
            if len(sync) == 2:
 +
                syncData[sync[0]] = sync[1]
 +
        oldSyncData = syncData.copy()
 +
        newContent = u''
 +
        previousIndex = 0
 +
        definedStrings = set()
 +
        for comment in self.commentsExtract.finditer(content):
 +
            newContent += content[previousIndex:comment.start()]
 +
            previousIndex = comment.end()
 +
            # Process current comment
 +
            newContent += <nowiki>u'<!--\n\n' + self.processComment(u(comment.group(1)).strip(), currentDict, definedStrings, syncData) + u'\n\n-->'</nowiki>
 +
        newContent += content[previousIndex:]
 +
        # Check for deleted strings
 +
        for k in oldSyncData:
 +
            if k not in definedStrings:
 +
                try:
 +
                    deletePage(currentDict + self.subpageSeparator + k, 'Removed deleted string "' + k + u'" from [[:' + currentDict + u']].')
 +
                except:
 +
                    pass
 +
                if k in syncData:
 +
                    del syncData[k]
 +
        self.updateSyncData(currentDict, syncData, 'Full update')
 +
        self.editCounts[currentDict] = 0
 +
        return newContent
 +
    def scheduledRun(self):
 +
        for d in self.dictionaries:
 +
            fixPage(d)
 +
dictUpdater = DictionaryUpdater()
 +
addFilter(dictUpdater)
 +
scheduleTask(dictUpdater.scheduledRun, 3)
  
=== Implement new Blueprint usage ===
+
=== Remove some templates on translated pages if they are absent on english page ===
  def blueprintUseFix(t, **kwargs):
+
def autoRemoveRecentAdditionTag(t, **kwargs):
      ClassTokenDict = {
+
    # "stub" is included here because translators often copy this template from the English page
          'Scout Token': 'Scout',
+
    # instead of replacing it with "trans" or "update trans".
          'Soldier Token': 'Soldier',
+
    # This wiki is English first, and as such, stub usage in translated pages is incorrect.
          'Pyro Token': 'Pyro',
+
    # In the future, it might be more useful to just replace "stub" with "trans" or "update trans" instead.
          'Demoman Token': 'Demoman',
+
    # - T 01/08/24
          'Heavy Token': 'Heavy',
+
    toRemoveTemplates = ('recent addition', 'ra', 'new item', 'unreleased', 'beta', 'stub')
          'Engineer Token': 'Engineer',
+
    equivalences = {
          'Medic Token': 'Medic',
+
        'ra': 'recent addition',
          'Sniper Token': 'Sniper',
+
        'new item': 'recent addition'
          'Spy Token': 'Spy',
+
    }
          'Class Token - Scout': 'Scout',
+
    if t.getName().lower() not in toRemoveTemplates or 'article' not in kwargs or '/' not in kwargs['article'].title:
          'Class Token - Soldier': 'Soldier',
+
        return t
          'Class Token - Pyro': 'Pyro',
+
    targetTemplate = t.getName().lower()
          'Class Token - Demoman': 'Demoman',
+
    if targetTemplate in equivalences:
          'Class Token - Heavy': 'Heavy',
+
        targetTemplate = equivalences[targetTemplate]
          'Class Token - Engineer': 'Engineer',
+
    englishArticle = page(kwargs['article'].title[:kwargs['article'].title.find('/')])
          'Class Token - Medic': 'Medic',
+
    try:
          'Class Token - Sniper': 'Sniper',
+
        englishContent = englishArticle.getWikiText()
          'Class Token - Spy': 'Spy',
+
        englishContent, englishTemplates, englishKeys = templateExtract(englishContent)
          }
+
        for t2 in englishTemplates.values():
      SlotTokenDict = {
+
            if t2.getName().lower() in toRemoveTemplates:
          'Primary Token': 'primary',
+
                foundTemplate = t2.getName().lower()
          'Secondary Token': 'secondary',
+
                if foundTemplate in equivalences:
          'Melee Token': 'melee',
+
                    foundTemplate = equivalences[foundTemplate]
          'PDA Token': 'pda2',
+
                if foundTemplate == targetTemplate:
          'PDA2 Token': 'pda2',
+
                    return t
          'Slot Token - Primary': 'primary',
+
        return None
          'Slot Token - Secondary': 'secondary',
+
    except:
          'Slot Token - Melee': 'melee',
+
        return t
          'Slot Token - PDA': 'pda2',
+
addTemplateFilter(autoRemoveRecentAdditionTag)
          'Slot Token - PDA2': 'pda2',
 
          }
 
      if 'article' not in kwargs:
 
          return t
 
      if t.getName().lower() != 'blueprint':
 
          return t
 
      if t.getParam('result-2') is not None:
 
          if t.getParam('ingredient-2') in ClassTokenDict and t.getParam('ingredient-3') in SlotTokenDict:
 
              autoresultstring = ClassTokenDict[t.getParam('ingredient-2')] + ' ' + SlotTokenDict[t.getParam('ingredient-3')]
 
          elif t.getParam('ingredient-3') in ClassTokenDict and t.getParam('ingredient-2') in SlotTokenDict:
 
              autoresultstring = ClassTokenDict[t.getParam('ingredient-3')] + ' ' + SlotTokenDict[t.getParam('ingredient-2')]
 
          elif t.getParam('ingredient-1') in ClassTokenDict and t.getParam('ingredient-2') in SlotTokenDict:
 
              autoresultstring = ClassTokenDict[t.getParam('ingredient-1')] + ' ' + SlotTokenDict[t.getParam('ingredient-2')]
 
          elif t.getParam('ingredient-1') in ClassTokenDict and t.getParam('ingredient-3') in SlotTokenDict:
 
              autoresultstring = ClassTokenDict[t.getParam('ingredient-1')] + ' ' + SlotTokenDict[t.getParam('ingredient-3')]
 
          elif t.getParam('ingredient-2') in ClassTokenDict and t.getParam('ingredient-1') in SlotTokenDict:
 
              autoresultstring = ClassTokenDict[t.getParam('ingredient-2')] + ' ' + SlotTokenDict[t.getParam('ingredient-1')]
 
          else:
 
              autoresultstring = ClassTokenDict[t.getParam('ingredient-3')] + ' ' + SlotTokenDict[t.getParam('ingredient-1')]
 
          t.delParam('ingredient-1')
 
          t.delParam('ingredient-2')
 
          t.delParam('ingredient-3')
 
          t.delParam('result')
 
          t.delParam('result-1')
 
          t.delParam('result-2')
 
          t.delParam('result-3')
 
          t.delParam('result-4')
 
          t.delParam('result-5')
 
          t.delParam('result-6')
 
          t.setParam('autoresult', autoresultstring)
 
      return t
 
  addTemplateFilter(blueprintUseFix)
 
  
 
== File filters ==
 
== File filters ==
=== [http://en.wikipedia.org/wiki/Pngcrush PNGCrush]/[http://jpegclub.org/ jpegtran] all PNG/JPG images ===
+
=== Crush all PNG/JPG images ===
  class imageCrushFilter:
+
Delegated to [[User:CrushBOT|CrushBOT]].
      def __init__(self):
 
          self.minRatio = 5 # Compression ratio threshold
 
          self.minByteDiff = 1024 # Byte difference threshold
 
          self.jpgScanMap = u'0:  0  0 0 0 ;1 2: 0  0 0 0 ;0:  1  8 0 2 ;1:  1  8 0 0 ;2:  1  8 0 0 ;0:  9 63 0 2 ;0:  1 63 2 1 ;0:  1 63 1 0 ;1:  9 63 0 0 ;2:  9 63 0 0 ;'.replace(u';', u';\n')
 
          self.filterName = 'Saved crush information'
 
          self.extractHash = compileRegex(r'\{\{(?:png)?crush\s*\|\s*(\w+?)\s*\|\s*(\w+?)\s*}}')
 
          try:
 
              subprocess.call(['pngcrush', '-version'])
 
              self.pngenabled = True
 
          except:
 
              print 'Warning: PNGCrush is not installed or not in $PATH'
 
              self.pngenabled = False
 
          try:
 
              subprocess.call(['jpegtran', '-h'])
 
              self.jpgenabled = True
 
          except:
 
              print 'Warning: jpegtran is not installed or not in $PATH'
 
              self.jpgenabled = False
 
      def getFileHash(self, filename):
 
          h = hashlib.md5()
 
          f = open(filename, 'rb')
 
          for i in f.readlines():
 
              h.update(i)
 
          f.close()
 
          return u(h.hexdigest())
 
      def deleteFile(self, *fs):
 
          for f in fs:
 
              try:
 
                  os.remove(tempFile)
 
              except:
 
                  pass
 
      def __call__(self, content, article, **kwargs):
 
          title = u(article.title).lower()
 
          if title[-4:] == '.png':
 
              isPNG = True
 
              if not self.pngenabled:
 
                  return content
 
          elif title[-5:] == '.jpeg' or title[-4:] == '.jpg':
 
              isPNG = False
 
              if not self.jpgenabled:
 
                  return content
 
          else:
 
              return content
 
          try: # This is a high-risk filter, lots of I/O, so wrap it in a big try
 
              filePage = wikitools.wikifile.File(wiki(), article.title)
 
              hashes = [u'', u'']
 
              hashResult = self.extractHash.search(content)
 
              hashTemplate = None
 
              if hashResult:
 
                  hashes = [u(hashResult.group(1)), u(hashResult.group(2))]
 
                  hashTemplate = <nowiki>u'{{crush|' + hashes[0] + u'|' + hashes[1] + u'}}'</nowiki>
 
              tempFile = getTempFilename()
 
              filePage.download(location=tempFile)
 
              oldHash = self.getFileHash(tempFile)
 
              if oldHash in hashes:
 
                  return content # Already worked on that one
 
              hashTemplate = <nowiki>u'{{crush|' + oldHash + u'|None}}'</nowiki>
 
              tempOutput = getTempFilename()
 
              if isPNG:
 
                  result = subprocess.call(['pngcrush', '-rem', 'gAMA', '-rem', 'cHRM', '-rem', 'iCCP', '-rem', 'sRGB', '-brute', tempFile, tempOutput])
 
              else:
 
                  mapFile = getTempFilename()
 
                  mapFileHandle = open(mapFile, 'wb')
 
                  mapFileHandle.write(self.jpgScanMap.encode('ascii')) # Onoz ASCII
 
                  mapFileHandle.close()
 
                  result = subprocess.call(['jpegtran', '-o', '-scans', mapFile, '-copy', 'none', '-progressive', '-outfile', tempOutput, tempFile])
 
                  self.deleteFile(mapFile)
 
              oldSize = os.path.getsize(tempFile)
 
              newSize = os.path.getsize(tempOutput)
 
              self.deleteFile(tempFile)
 
              if not result and oldSize > newSize:
 
                  # Ready to upload... or are we?
 
                  ratio = int(round(100 * (1.0 - float(newSize) / float(oldSize))))
 
                  if ratio >= self.minRatio or oldSize - newSize >= self.minByteDiff:
 
                      newHash = self.getFileHash(tempOutput)
 
                      if newHash in hashes:
 
                          self.deleteFile(tempOutput)
 
                          return content # Already got that result, no need to reupload
 
                      hashTemplate = <nowiki>u'{{crush|' + oldHash + u'|' + newHash + u'}}'</nowiki>
 
                      uploadFile(tempOutput, u(article.title), u'Crushed version: ' + u(ratio) + u'% reduction / ' + u(oldSize - newSize) + u' bytes saved; from ' + u(oldSize) + u' to ' + u(newSize) + u' bytes.', overwrite=True, reupload=True)
 
                      hashes = [oldHash, newHash]
 
              if hashResult:
 
                  content = content[:hashResult.start()] + hashTemplate + content[hashResult.end():]
 
              else:
 
                  content = content.strip() + u'\n\n' + hashTemplate
 
              self.deleteFile(tempOutput)
 
          except:
 
              pass # Well, that didn't work
 
          return content
 
  #Disabled: addFileFilter(imageCrushFilter())
 
  
 
== Additional tasks ==
 
== Additional tasks ==
=== Update {{tl|Blog link}} ===
+
=== Update {{tl|Dictionary/defindex}} and {{tl|Dictionary/price}} ===
  def updateTemplateBlogLink():
+
'''TEMPORARILY DISABLED:''' WindBOT can't seem to properly fetch the item schema (probably an issue in steamGetGameSchema()), which causes this function to fail.
      fConfig = {
+
 
          'page': 'Template:Blog link',
+
[[User:PhoneWave|PhoneWave]] is taking care of this for now. See [[User:PhoneWave/forceDictionary/defindex]].
          'begin': u'<!--THIS IS GENERATED BY WINDBOT-->',
+
 
          'end': u'<!--NO MORE GENERATED BY WINDBOT-->',
+
def updateGameDictionaries():
          'firstElement': u'[http://www.teamfortress.com/post.php?id={{#switch:{{{1|ERROR}}}',
+
if steam is None:
          'lastElement': u'#default = ERROR}} {{{2|{{{1|blog post}}}}}}]',
+
return
          'rss': 'http://www.teamfortress.com/rss.xml',
+
# Begin configurable section
          'linkRegex': compileRegex(r'id=(\d+)'),
+
game = 440
          'timeoffset': -7
+
priceDictionaryPage = u'Template:Dictionary/price'
      }
+
indexDictionaryPage = u'Template:Dictionary/defindex'
      bl = page(fConfig['page'])
+
itemEquivalences = {
      content = u(bl.getWikiText())
+
u'taunt: the (.*)': u'\\1',
      originalContent = content
+
u'taunt: (.*)': u'\\1',
      begin, end = content.find(fConfig['begin']), content.find(fConfig['end'])
+
u'taunt: the (.*)': u'\\1 (taunt)', # Useful for Meet the Medic
      if begin == -1 or end == -1:
+
u'taunt: (.*)': u'\\1 (taunt)', # Useful for Meet the Medic
          return # Couldn't find begin/end
+
u'strange filter: (.*)': u'strange filter - \\1'
      try:
+
}
          feed = feedparser.parse(fConfig['rss'])
+
forceDefindex = {
      except:
+
u'champ stamp': 815,
          return
+
u'confidential collection case': 5823,
      results = [fConfig['firstElement']]
+
u'fall 2013 acorns crate key': 5710,
      entries = 0
+
u'flying guillotine': 812,
      for i in feed.entries:
+
u'gargoyle key': 5827,
          res = fConfig['linkRegex'].search(i.link)
+
u'gloves of running urgently': 239,
          date = datetime.datetime.fromtimestamp(time.mktime(i.updated_parsed)) + datetime.timedelta(hours=fConfig['timeoffset'])
+
u'human cannonball': 817,
          if res:
+
u'huo-long heater': 811,
              entries += 1
+
u'lugermorph': 160,
              day = date.strftime('%d')
+
u'mann co. stockpile crate': 5738,
              if day[0] == '0':
+
u'mann co. stockpile crate key': 5740,
                  day = day[1:]
+
u'mann co. store package': 729,
              results.append(u(date.strftime('%B ') + day + date.strftime(', %Y') + ' = ' + res.group(1)))
+
u'mann co. strongbox key': 5720,
      results.append(fConfig['lastElement'])
+
u'mann co. supply crate key': 5021,
      results = u'\n | '.join(results)
+
u'marxman': 816,
      content = content[:begin+len(fConfig['begin'])] + results + content[end:]
+
u'neon annihilator': 813,
      if content != originalContent and entries:
+
u'pda': 737,
          editPage(bl, content, summary=u'Updated blog post list.')
+
u'pyrovision goggles': 743,
  scheduleTask(updateTemplateBlogLink, 10)
+
u'quarantined collection case': 5822,
 +
u'red-tape recorder': 810,
 +
u'something special for someone special': 5074,
 +
u'triad trinket': 814,
 +
u'what\'s in the team fortress 2 soundtrack box?': 1176,
 +
u'love and war cosmetics bundle': 2131,
 +
}
 +
decimalSeparator = <nowiki>'{{dec}}'</nowiki>
 +
illegalItemCharacters = compileRegex(<nowiki>r'[:%#\r\n\t]+'</nowiki>)
 +
priceDictionaryHeader = <nowiki>u'{{dictionary/header}}\n== /price ==\n\'\'\'This dictionary is automatically updated by WindBOT\'\'\'. Edits made to this page will be overwritten.\n<!--\n'</nowiki>
 +
indexDictionaryHeader = <nowiki>u'{{dictionary/header}}\n== /defindex ==\n\'\'\'This dictionary is automatically updated by WindBOT\'\'\'. Edits made to this page will be overwritten.\n<!--\n'</nowiki>
 +
dictionaryFooter = <nowiki>u'\n-->'</nowiki>
 +
# End configurable section
 +
schema = steamGetGameSchema(game)
 +
assets = steamGetGameAssets(game)
 +
dictionary = {}
 +
defindex = {}
 +
for item in schema:
 +
itemName = u(item.name).lower().replace(u'_', u' ')
 +
keys = [itemName]
 +
for r in itemEquivalences:
 +
itemName = re.sub(u'^' + r + u'$', itemEquivalences[r], itemName)
 +
if itemName not in keys:
 +
keys.append(itemName)
 +
itemKeys = u' | '.join([illegalItemCharacters.sub(u<nowiki>''</nowiki>, x) for x in keys])
 +
defindex[itemName] = itemKeys + u': ' + u(item.schema_id)
 +
if item not in assets:
 +
continue
 +
prices = assets[item.schema_id].price
 +
if not len(prices):
 +
continue
 +
prices = dict((u(k).strip().lower(), v) for k, v in prices.iteritems()) # Sanitize data
 +
dictionary[itemName] = itemKeys + u':'
 +
for currency in sorted(prices):
 +
price = prices[currency]
 +
if price.is_integer():
 +
price = u(int(price))
 +
else:
 +
price = u(round(price, 2))
 +
dot = price.find(u'.')
 +
if dot == len(price) - 2:
 +
price += u'0'
 +
dictionary[itemName] += u'\n  ' + u(currency).lower() + u': ' + price.replace('.', decimalSeparator)
 +
for item in forceDefindex:
 +
defindex[u(item)] = illegalItemCharacters.sub(u<nowiki>''</nowiki>, u(item)) + u': ' + u(forceDefindex[item])
 +
if len(dictionary):
 +
# Is updating time doktor
 +
finalPage = priceDictionaryHeader
 +
for item in sorted(dictionary):
 +
finalPage += u'\n' + dictionary[item] + u'\n'
 +
finalPage += dictionaryFooter
 +
priceDictionaryPage = page(priceDictionaryPage)
 +
editPage(priceDictionaryPage, finalPage, summary=u'Updated item prices from WebAPI.', minor=True, bot=True, nocreate=True)
 +
# Run other filters on the new page
 +
fixPage(priceDictionaryPage)
 +
if len(defindex):
 +
# Is also updating time doktor
 +
finalPage = indexDictionaryHeader
 +
for item in sorted(defindex):
 +
finalPage += u'\n' + defindex[item] + u'\n'
 +
finalPage += dictionaryFooter
 +
indexDictionaryPage = page(indexDictionaryPage)
 +
editPage(indexDictionaryPage, finalPage, summary=u'Updated item indexes from WebAPI.', minor=True, bot=True, nocreate=True)
 +
# Run other filters on the new page
 +
fixPage(indexDictionaryPage)
 +
# scheduleTask(updateGameDictionaries, 2)
 +
 
 +
=== Update [[Lastpatch]] and [[Lastpatchbeta]] ===
 +
# Redirect Updater Class - modified from <nowiki>http://github.com/i-ghost/wikiscripts</nowiki>
 +
# To update the lastpatch redirects on Team Fortress Wiki, but could probably be deployed on The Portal Wiki/Dota 2 Wiki
 +
# i-ghost
 +
 +
class redirectUpdater(object):
 +
def __init__(self, updateTemplateName="Template:Updates", pageName="Lastpatch", betaPageName="Lastpatchbeta"):
 +
self.pageName = pageName
 +
self.betaPageName = betaPageName
 +
self.updateTemplateName = updateTemplateName
 +
self.pageText = page(self.pageName).getWikiText()
 +
self.betaPageText = page(self.betaPageName).getWikiText()
 +
self.updateTemplateText = page(self.updateTemplateName).getWikiText().split("\n")
 +
self.langs = ["ru", "fr", "de", "pl", "pt-br", "fi", "es", "nl", "zh-hans", "zh-hant", "ar", "cs", "da", "hu", "it", "ja", "ko", "no", "pt", "ro", "sv", "tr"]
 +
self._get_dates()
 +
self.patch = "%s %s, %s" % (self._get_month(int(self.updates["patch-month"])), self.updates["patch-day"], self.updates["patch-year"])
 +
self.betaPatch = "%s %s, %s" % (self._get_month(int(self.updates["patch-beta-month"])), self.updates["patch-beta-day"], self.updates["patch-beta-year"])
 +
self.footer = <nowiki>"<!-- This page is automatically generated when %s is modifed. Do not modify this page. -->"</nowiki> % (self.updateTemplateName)
 +
 +
def _get_month(self, month):
 +
"""WindBOT doesn't import calendar, so we use this instead"""
 +
return datetime.date(1960, month, 1).strftime("%B")
 +
 +
def _get_dates(self):
 +
"""Internal: Gets dates from update template and stores in dictionary"""
 +
self.updates = {}
 +
for i in self.updateTemplateText:
 +
if i.find("|") == 0:
 +
# Liable to break
 +
self.updates[i.lstrip("|").replace(" ", "").partition("=")[0]] = i.replace(" ", "").partition("=")[2].replace(<nowiki>"<!--Don'tforgettoupdateme!-->"</nowiki>, "")
 +
 +
def make_edit_strings(self, beta=False, lang=False):
 +
"""Creates the final page content."""
 +
<nowiki>if beta and lang:
 +
self.redirect_string = "#REDIRECT [[%s Patch (Beta)/%s]] {{R lang|%s}}" % (self.betaPatch, lang, lang)
 +
self.summary_string = "Updated [[%s/%s]] redirect to [[%s Patch (Beta)/%s]]" % (self.betaPageName, lang, self.betaPatch, lang)
 +
elif beta and not lang:
 +
self.redirect_string = "#REDIRECT [[%s Patch (Beta)]]" % (self.betaPatch)
 +
self.summary_string = "Updated [[%s]] redirect to [[%s Patch (Beta)]]" % (self.betaPageName, self.betaPatch)
 +
 +
if lang and not beta:
 +
self.redirect_string = "#REDIRECT [[%s Patch/%s]] {{R lang|%s}}" % (self.patch, lang, lang)
 +
self.summary_string = "Updated [[%s/%s]] redirect to [[%s Patch/%s]]" % (self.pageName, lang, self.patch, lang)
 +
elif not lang and not beta:
 +
self.redirect_string = "#REDIRECT [[%s Patch]]" % (self.patch)
 +
self.summary_string = "Updated [[%s]] redirect to [[%s Patch]]" % (self.pageName, self.patch)</nowiki>
 +
 +
def _update_redirect(self, beta=False, lang=False):
 +
"""Internal: Provides editing functionality"""
 +
self.make_edit_strings(beta, lang)
 +
# Use the correct page
 +
if beta:
 +
if lang:
 +
_pagetoedit = "%s/%s" % (self.betaPageName, lang)
 +
else:
 +
_pagetoedit = self.betaPageName
 +
else:
 +
if lang:
 +
_pagetoedit = "%s/%s" % (self.pageName, lang)
 +
else:
 +
_pagetoedit = self.pageName
 +
# Send the edit
 +
editPage(_pagetoedit, "%s\n%s" % (self.redirect_string, self.footer), summary=self.summary_string, minor=True, nocreate=False)
 +
 +
def check_if_update_needed(self, beta=False):
 +
"""Checks if redirects needs updating"""
 +
try:
 +
if beta:
 +
if self.betaPageText.split("[[")[1].partition("]]")[0].rstrip("Patch (Beta)") != self.betaPatch:    return True
 +
else:
 +
if self.pageText.split("[[")[1].partition("]]")[0].rstrip(" Patch") != self.patch:    return True
 +
except IndexError:
 +
return True # Just update anyway
 +
 +
def update(self, beta=False):
 +
"""Updates the redirects and their lang pages"""
 +
if beta:
 +
self._update_redirect(beta=True)
 +
for lang in self.langs:
 +
self._update_redirect(beta=True, lang=lang)
 +
else:
 +
self._update_redirect()
 +
for lang in self.langs:
 +
self._update_redirect(lang=lang)
 +
 +
def run(self):
 +
"""Runs everything"""
 +
# Beta
 +
if self.check_if_update_needed(beta=True):
 +
self.update(beta=True)
 +
# Normal
 +
if self.check_if_update_needed():
 +
self.update()
 +
 +
scheduleTask(redirectUpdater().run, 2)
 +
 
 +
=== Update game prices ===
 +
Delegated to [[User:PhoneWave|PhoneWave]]. See [[User:PhoneWave/forceDictionary/gameinfo]] for gameids.
  
=== Update [[List of Item Attributes]] ===
+
=== Update {{tl|Dictionary/steam ids}} ===
  def updateItemAttributes():
 
      try:
 
          attributes = <nowiki>urllib2.urlopen('http://optf2.com/attrib_dump?wikitext=1').read(-1)</nowiki>
 
      except:
 
          return
 
      if not attributes or attributes.find('<!DOCTYPE') != -1:
 
          return
 
      <nowiki>editPage('List of Item Attributes', '{{:List of Item Attributes/Header}}\n' + attributes + '\n{{:List of Item Attributes/Footer}}', summary=u'Updated item attributes', minor=True)</nowiki>
 
  scheduleTask(updateItemAttributes, 20)
 
  
=== Update {{tl|Item checklist}} on [[User:WindBOT/Item checklists|list of subscribers]] ===
+
def updatePlayerInfoDictionary(): # By Smashman
  def itemChecklists():
+
    """A WindBOT filter created to fetch Steam user's name and vanity information and store it in Template:Dictionary/steam_ids"""
      def updateItemChecklist(checklist, schema, support):
+
    # Begin configurable section
          if not checklist.getParam('steamid'):
+
    <nowiki>playerInfoDictionaryHeader = u'{{dictionary/header}}\n== /steam_ids ==\n\'\'\'The information in this dictionary is automatically updated by WindBOT. Add new Steam IDs in [[Template:Dictionary/steam ids/id list]]\'\'\'.\n<!--\n'
              checklist.setParam('error', 'Unspecified Steam ID.')
+
    playerListDictionaryHeader = u'{{dictionary/header}}\n== /steam_ids/id_list ==\n\'\'\'The information in this dictionary is tracked by WindBOT. Add new Steam IDs to the bottom of this list\'\'\'.\n\n'
              return True
+
    playerInfoDictionaryFooter = u'\n-->'</nowiki>
          supportedItems = {}
+
    # End configurable section
          for i in support:
+
    # Is updating time doktor
              supportedItems[i] = 0
+
    playerInfoDictionaryPage = u'Template:Dictionary/steam ids'
          try:
+
    playerListDictionaryPage = u'Template:Dictionary/steam ids/id list'
              steamUser = steam.user.profile(checklist.getParam('steamid'))
+
    id_list = page('Template:Dictionary/steam ids/id list').getWikiText()
              backpack = steam.tf2.backpack(steamUser, schema=schema)
+
    id64s=sorted(frozenset(filter(lambda x: x.isdigit(), id_list.splitlines())))
          except steam.user.ProfileError as e:
+
    listPage = playerListDictionaryHeader + '\n'.join(map(str, id64s)) + '\n'
              checklist.setParam('error', u(e))
+
    editPage(playerListDictionaryPage, listPage, summary=u'Sort tracked Steam IDs.', minor=True, bot=True, nocreate=True)
              return True
+
    start_limit = 0
          except steam.tf2.TF2Error as e:
+
    end_limit = 100
              if u(e) in (u'Bad SteamID64 given', u'Profile set to private'):
+
    i=0
                  checklist.setParam('error', u(e))
+
    div_100 = (len(id64s)/100)+1 #Times to repeat sections
                  return True
+
    finalPage = playerInfoDictionaryHeader
              return False
+
    while i != div_100:
          for item in backpack:
+
        id64_sort = sorted(id64s[start_limit:end_limit]) #Numerically sort the ids, for use in call
              itemName = u(item.get_name()).lower()
+
        id64s_csv = ','.join(id64_sort) #Put them into a csv string
              if itemName in supportedItems:
+
        data = steam.api.interface("ISteamUser").GetPlayerSummaries(version = 2, steamids = id64s_csv) #Do the call
                  supportedItems[itemName] += 1
+
        data = sorted(data["response"]["players"], key=lambda x: x['steamid']) #Numerically sort by id
          for item in supportedItems:
+
        for player in data:
              if supportedItems[item] > 1:
+
            purl = player["profileurl"].strip('/')
                  checklist.setParam(item, supportedItems[item])
+
            finalPage += u"\n{0}:".format(player["steamid"])
              elif supportedItems[item] == 1:
+
            <nowiki>finalPage += u("\n  nickname: <no" + u"wiki>{0}</no" + u"wiki>").format(player["personaname"].replace(u'<!--', u'&lt;!--').replace(u'-->', u'--&gt;'))</nowiki>
                  checklist.setParam(item, 'yes')
+
            if purl.find("/id/") != -1:
              else:
+
                vanity = os.path.basename(purl)
                  p = checklist.getParam(item)
+
                finalPage += u"\n  vanity: {0}".format(vanity)
                  if p is not None:
+
            finalPage += u"\n"
                      p = p.lower()
+
        start_limit += 100
                  if p in (None, 'no', '0'):
+
        end_limit += 100
                      checklist.setParam(item, 'no')
+
        i+=1
                  elif p not in ('wanted', 'want', 'do not', 'anti', 'do not want'):
+
    finalPage += playerInfoDictionaryFooter
                      checklist.setParam(item, 'had')
+
    editPage(playerInfoDictionaryPage, finalPage, summary=u'Updated steam player information from WebAPI.', minor=True, bot=True, nocreate=True)
          return True
+
    fixPage(playerInfoDictionaryPage)
      try:
+
scheduleTask(updatePlayerInfoDictionary, 16)
          # *ahem* Because steamodd won't use singletons, requiring manual management of the schema object by the library user
+
def updateDictionaryOnSteamIdListUpdate(content, **kwargs):
          schema = steam.tf2.item_schema()
+
    if 'article' in kwargs and kwargs['article'] == 'Template:Dictionary/steam ids/id list':
          allItems = []
+
        updatePlayerInfoDictionary()
          for item in schema:
+
    return content
              allItems.append(u(item.get_name()).lower())
+
addFilter(updateDictionaryOnSteamIdListUpdate)
      except:
 
          return # No schema means no fancy
 
      support = []
 
      templateParams = compileRegex(r'\{\{\{\s*([^{}|]+?)\s*\|')
 
      templateCode = page('Template:Item checklist').getWikiText()
 
      res = templateParams.search(templateCode)
 
      while res:
 
          item = u(res.group(1)).lower()
 
          if item not in support and item in allItems:
 
              support.append(item)
 
          templateCode = templateCode[res.end():]
 
          res = templateParams.search(templateCode)
 
      checkPage, checkLinks = linkExtract(page('User:WindBOT/Item_checklists').getWikiText())
 
      for i in range(3):
 
          checklist = page(random.choice(checkLinks).getLink())
 
          print 'Updating', checklist
 
          update = False
 
          oldContent = u(checklist.getWikiText())
 
          content, templatelist = templateExtract(oldContent)
 
          for t in templatelist:
 
              if t.getName().lower() == u'item checklist':
 
                  update = updateItemChecklist(t, schema, support)
 
                  break
 
          content = templateRestore(content, templatelist)
 
          if update and oldContent != content:
 
              editPage(checklist, content, summary=<nowiki>u'Updated Item checklist [[:' + u(checklist.title) + u']]'</nowiki>, minor=True)
 
  scheduleTask(itemChecklists, 2)
 

Latest revision as of 16:21, 7 December 2024

How to disable a filter

If the bot is malfunctioning, chances are that the problem lies in one of these blocks of code. Thus, instead of shutting down the whole bot, it would be wiser to disable only the chunk of code that is misbehaving. To make the bot ignore a certain line, add a "#" in front of it:

# This line will be ignored

If there are multiple lines, wrap them inside triple-quotes (you still need to put the two spaces at the beginning of the line):

"""This line will be ignored
and this one as well
and this one is cake
and the previous one was a lie but it was still ignored"""

If all else fails, you can simply delete the block from the page. The bot can't come up with code by itself yet, so it won't run anything. Or, if the problem really is elsewhere, block the bot.

Contents

Global filters

File categorization (Add {{No category}} or {{User image}})

def fileCategorization(content, **kwargs):
	badLoneCategories = [u'Category:Screenshot images', u'Category:Artwork images', u'Category:Multimedia files', u'Category:Extracted media', u'Category:Images without license tags', u'Category:Images that need improving', u'Category:Images using ((Another license)) incorrectly']
	extraStuff = u'{{No category}}'
	userImageCategory = u'[[Category:User images]]'
	userAudioCategory = u'[[Category:User audio]]'
	userTextCategory = u'[[Category:User text]]'
	regLang = compileRegex('/[^/]+$')
	if 'article' not in kwargs or content.lower().find(u'#redirect') != -1:
		return content
	title = u(kwargs['article'].title)
	if title[:5].lower() != u'file:':
		return content
	categories = kwargs['article'].getCategories()
	if title.lower().startswith(u'file:user '):
		if title.lower().endswith(('.mp3', '.wav', '.ogg', '.flac')) and u'Category:User audio' not in categories and userAudioCategory not in content:
			return (content.strip() + u'\n' + userAudioCategory).strip()
		if title.lower().endswith(('.txt', '.cfg', '.diff', '.patch')) and u'Category:User text' not in categories and userTextCategory not in content:
			return (content.strip() + u'\n' + userTextCategory).strip()
		if title.lower().endswith(('.png', '.gif', '.jpg', '.jpeg', '.webp', '.apng', '.psd', '.webm')) and u'Category:User images' not in categories and userImageCategory not in content:
			return (content.strip() + u'\n' + userImageCategory).strip()
		return content # Don't go further
	for c in categories:
		if u(regLang.sub(u'', c)) not in badLoneCategories:
			return content
	if not len(content):
		return extraStuff
	if content.find(extraStuff) != -1:
		return content
	return content.strip() + u'\n' + extraStuff
addFilter(fileCategorization)

Page filters

addPageFilter(r'^user:', r'(?:talk|help|wiki|template|module):')

Semantic filters

Item names

categories = [ # These are categories that contain that name of things that should be capitalized
    'Weapons',
    'Hats',
    'Miscellaneous items',
    'Buildings'
]
exceptions = [ # Those are page names that are in the above categories but should not count as capitalized weapon names
    'Direct Hit',
    'Hats', 'Weapons', 'Miscellaneous items', 'Buildings',
    'Item levels',
    'Developer weapons',
    'Self-made items',
    'Unused content',
    'Promotional items',
    'Mercenary', 'Taunts', 'Decapitation', 'Fists', 'Cloak', 'One-Man Army', # Too common to be reliably replaced
    'Club', 'Bat', 'Knife', 'Syringe', 'Bottle', 'Original', # Ditto (weapons)
    'Classified', # Not capitalized even when referring to the hat
    'Attendant', # Avoids issue with French
    'Buff Banner', 'Medi Gun', 'Bonk Helm', 'B.A.S.E. Jumper', # Avoids issue with German
    'Voodoo Juju', # Conflicts Juju/JuJu
    'Reskins' # Not capitalized
]
for c in categories:
    c = wikitools.category.Category(wiki(), u(c))
    for p in pageFilter(c.getAllMembers(titleonly=True)):
        if p not in exceptions:
            enforceCapitalization(p)
# Special case: Direct Hit
addSafeFilter(wordFilter('Direct Hit', r'(?<!a )(?<!one )(?<!on )(?<!\()(?<!placed )(?<!single )\bDirect Hit'))
# Special case: Attendant
enforceCapitalization('Attendant', languageBlacklist=['fr'])
# Special cases: "Buff-Banner"/"Medigun" in German
enforceCapitalization('Buff Banner', 'Medi Gun', languageBlacklist=['de'])
enforceCapitalization('Buff-Banner', 'Medigun', language='de')
# Put "an" in front of Electro Sapper
addSafeFilter(
    dumbReplace('an Sapper', 'a Sapper'),
    dumbReplace('An Sapper', 'A Sapper')
)
# Flare Gun
addSafeFilter(wordFilter('Flare Gun', 'flaregun'))

Classes

# Not "Heavy" because the word can be used as an adjective:
classes = ['Scout', 'Soldier', 'Pyro', 'Demoman', 'Heavy Weapons Guy', 'Engineer', 'Medic', 'Sniper'] # Spy added later
classesPlurals = ['Scouts', 'Pyros', 'Demomen', 'Heavy Weapons Guys', 'Heavies', 'Engineers', 'Snipers', 'Spies']
enforceCapitalization(*classes)
enforceCapitalization(*classesPlurals)
# Special Spy case:
addSafeFilter(wordFilter('Spy', '(?<!I )Spy'))
classes.append('Spy') # Used in later functions with Spy included

Other capitalized words

enforceCapitalization('Team Fortress 2', 'TF2')
enforceCapitalization('Payload', 'Overtime', 'Dispensers', 'Sappers', 'Crits', 'Crit')
enforceCapitalization('PlayStation', 'Xbox')
enforceCapitalization('iPod', 'iPhone')

Word aliases

addSafeFilter(
    wordFilter(u'Ubersaw', u'[üÜ]bersaw'), # Ubersaw
    wordFilter(u'Force-a-Nature', u'Force of Nature'), # Force-A-Nature
    wordFilter(u'ÜberCharges', u'[Üüu]ber(?!säge)(?!sage)(?! Entertainment)(?: ?charge)?s'), # ÜberCharges
    wordFilter(u'Intelligence', u'Intel(?!\\s*(?:CPU|processor))'), # Intelligence (maybe adding "flag" in there would be too agressive)
    wordFilter(u'Sapper', u'(?:Ele[ck]tro |Ultra )+Sapper', u'Electro-?Sapper'),
    wordFilter(u'Heavy Weapons Guy', u'Heavy Guy'),
    wordFilter(u'Intelligence room', u'Intel(?:ligence)? room'),
    wordFilter(u'Intelligence briefcase', u'Intel(?:ligence)? briefcase'),
    wordFilter(u'K.G.B.', u'KGB'),
    wordFilter(u'Chieftain', u'Chieftian', u'Chieftan'),
    wordFilter(u'Batallion\'s Backup', u'Batallion\'?s? Back-?up'),
    wordFilter(u'Mann-Conomy', u'Mann?-?Conomy'),
    wordFilter(u'First-person view', u'1(?:st)? person view'),
    wordFilter(u'Über Update', u'[UÜü]ber update')
    #wordFilter(u'Stickybomb', u'Stickybomb', u'Sticky bomb'),
    #wordFilter(u'Stickybombs', u'Stickybombs', u'Sticky bombs')
)
addSafeFilter(
    wordFilter(u'Medi Gun', u'Medi-?Gun'),
    languageBlacklist=['fr', 'de', 'pl']
)

Sentry Gun

addSafeFilter(
    wordFilter(u'Sentry Gun', r"(?<!Mini.)(?<!Mah.)(?<!My.)Sentry(?![-\s]*(?:Gun|here|ahead|forward|up there|right up|, right up|'{2,}|jump)|-)"),
    wordFilter(u'Sentry Guns', r'Sentry Guns', r'Sentries(?!\s+Gun)'),
    wordFilter(u'Combat Mini-Sentry Gun', r'Combat Mini-Sentry Gun', r'(?:(?:Mini|Combat)\s+)+Sentry(?!\s+Gun|-)'),
    wordFilter(u'Combat Mini-Sentry Guns', r'(?:(?:Mini|Combat)[-\s]+)+Sentr(?:y[-\s]+Guns|ies)')
)
addFilter(regex(r'\[\[([^][|]+\|)?(?:Sentry Guns|Sentries)\]\]', '$1Sentry Guns')) # Put the "s" out of the link

Common misspellings

addSafeFilter(
    wordFilter('Huntsman', 'Hunstman'),
    wordFilter('Dispenser', 'Dis?pen[sc][eo]r'),
    wordFilter('Heavy', 'Hevy'),
    wordFilter('Engineer', 'Enginer'),
    wordFilter('Soldier', 'Solider'),
    wordFilter('Mini-Crit', 'Minicrit'),
    wordFilter('Flame Thrower', 'Flamethrower'),
    wordFilter('Mini-Crits', 'Minicrits'),
    wordFilter('Mini-Crit', 'Mini-crit'),
    wordFilter('Mini-Crits', 'Mini-crits'),
    wordFilter('Critical hit', 'Critical Hit'),
    wordFilter('Critical hits', 'Critical Hits'),
    wordFilter('Chargin\' Targe', 'charg[ei][-\'n\\s]*targe?'),
    wordFilter('Kritzkrieg', 'Kritzkreig'),
    wordFilter('screenshot', 'screen shot'),
    wordFilter('screenshots', 'screen shots'),
    wordFilter('in-game', 'ingame'),
    # wordFilter('team-colored', 'team colou?red', keepcapitalization=True)
    # wordFilter('color', '(?<!Rustic )colour')
)
addSafeFilter(
    wordFilter('Natascha', 'Natas?c?ha'),
    language='en'
)
addSafeFilter(
    wordFilter(u'tradable', u'tradeable', keepcapitalization=True),
    language='en'
)
addSafeFilter(
    wordFilter('Spies', 'Spys'),
    languageBlacklist=['de', 'es']
)

Map names

addSafeFilter(
    #wordFilter('Gravel Pit', '(?<!cp_)Gravelpit', 'Gravel pit'), - Conflicts with Gravelpit Emperor and its various translations
    wordFilter('Badwater Basin', '(?<!pl_)Badwater Basin', '(?<!pl_)Badwater(?!\s+Basin)'),
    wordFilter('Gold Rush', '(?<!pl_)Goldrush')
)
# Capitalisation rules for map names are whitelist-based rather than Category:Maps+blacklist based, because lots of maps are common nouns.
# As such, if a map with a non-common name is added, please add it to this list.
enforceCapitalization(
    '2Fort', 'Badlands', 'Coldfront', 'Dustbowl', 'Egypt', 'Fastlane', 'Granary', 'Gullywash',
    'Hightower', 'Hoodoo', 'Landfall', 'Offblast', 'Thunder Mountain',
    'Viaduct', 'Wildfire', 'Yukon'
)

Section headers

addSafeFilter(
    wordFilter(u'== Update history ==', u'==+ ?(?:Update history|Previous changes) ?==+'),
    wordFilter(u'== See also ==', u'==+ ?See also ?==+'),
    wordFilter(u'== External links ==', u'==+ ?External links ?==+'),
    wordFilter(u'== Painted variants ==', u'==+ ?Painted variants ?==+'),
    wordFilter(u'== Item set ==', u'==+ ?Item set ?==+'),
    wordFilter(u'== Damage and function times ==', u'==+ ?Damage and function times ?==+'),
    wordFilter(u'=== As a crafting ingredient ===', u'==+ ?As a crafting ingredient ?==+'),
    wordFilter(u'== Unused content ==', u'==+ ?Unused content ?==+'),
    wordFilter(u'== See also ==', u'==+ ?See also ?==+'),
    wordFilter(u'== Related achievements ==', u'==+ ?Related achievements ?==+'),
    wordFilter(u'== Strange variant ==', u'==+ ?Strange variant ?==+'),
    wordFilter(u'=== Undocumented changes ===', u'==+ ?Undocumented changes ?==+')
)

Language-specific filters

Language-agnostic

# Language-agnostic achievements auto-translate
def translateAchievements(language, pageSuffix):
    try:
        tf = readLocaleFile(urllib2.urlopen(page('File:Tf_' + language + '.txt').getDownloadUrl()).read(-1))
    except:
        print 'Downloading failed.'
        return
    try:
        languages = parseLocaleFile(tf, language=language)
    except:
        print 'Error while parsing tf_' + language + '.txt'
        return
    acceptPrefix = ['TF_SCOUT_', 'TF_SOLDIER_', 'TF_PYRO_', 'TF_DEMOMAN_', 'TF_HEAVY_', 'TF_ENGINEER_', 'TF_MEDIC_', 'TF_SNIPER_', 'TF_SPY_', 'TF_GET_', 'TF_KILL_', 'TF_BURN_', 'TF_WIN_', 'TF_PLAY_']
    acceptSuffix = ['_NAME', '_DESC']
    filteredAchievements = languagesFilter(languages, commonto=[language, 'english'], prefix=acceptPrefix, suffix=acceptSuffix, exceptions=['TF_SOLDIER_ASSIST_MEDIC_UBER_NAME'])
    associateLocaleWordFilters(filteredAchievements, 'english', language, pageSuffix)

Romanian filters

# Romanian characters
addSafeFilter(
    dumbReplaces({
        u'ş': u'ș',
        u'ţ': u'ț'
    }), language='ro'
)
# Romanian achievements (disabled for now)
#translateAchievements('romanian', 'ro')
# Fix User:Vulturas's stoopid:
addSafeFilter(
    wordFilter(u'batjocură', u'batjocoră'),
    wordFilter(u'batjocura', u'batjocora'),
    wordFilter(u'batjocureşte', u'batjocoreşte', u'batjocorește'),
    wordFilter(u'batjocuri', u'batjocori'),
    wordFilter(u'batjocurile', u'batjocorile'),
    wordFilter(u'batjocurii', u'batjocorii'),
    language='ro'
)

German filters

addSafeFilter( # Requested by Picard
    wordFilter(u'Krit-a-Cola', u'(?<!Bonk! )[CK]rit-\'?[an]\'?-Cola'),
    wordFilter(u'Bonk! Krit-\'n-Cola', u'Bonk! [CK]rit-\'?[an]\'?-Cola'),
    wordFilter(u'Krit', u'Crit'),
    wordFilter(u'Krits', u'Crits'),
    wordFilter(u'Sonstiger Gegenstand', u'Diverser Gegenstand', u'diverser Gegenstand', u'sonstiger Gegenstand'),
    wordFilter(u'Sonstige Gegenstände', u'Diverse Gegenstände', 'diverse Gegenstände', 'sonstige Gegenstände'),
    language='de'
)
# German achievements (disabled for now)
#translateAchievements('german', 'de')
addSafeFilter(
    wordFilter(u'== Update-Verlauf ==', u'==+ *(?:Update Verlauf|Letzte [Ääa]nderungen) *==+'), language='de'
)

Spanish filters

addSafeFilter(
    wordFilter(u'Forajido', u'Gunslinger'),
    wordFilter(u'Pistolón', u'Gran Masacre'),
    wordFilter(u'Medic', u'M[eé]dico(?! Medieval)'),
    wordFilter(u'Medics', u'M[eé]dicos(?! Medieval)'),
    wordFilter(u'Sniper', u'(?<!Rifle de )Francotirador'),
    wordFilter(u'Snipers', u'(?<!Rifle de )Francotiradore?s'),
    wordFilter(u'Spy', u'Esp[ií]a'),
    wordFilter(u'Spies', u'Spys', u'Esp[ií]as'),
    wordFilter(u'Soldier', u'Soldado(?! de Fortuna)'),
    wordFilter(u'Soldiers', u'Soldados'),
    wordFilter(u'Engineer', u'(?<!Gorra de )Ingeniero'),
    wordFilter(u'Engineers', u'Ingenieros'),
    wordFilter(u'Curiosidades', u'Trivia'),
    wordFilter(u'Actualizacion Mann-Conomy', u'Mann-Conomy Update'),
    wordFilter(u'Übersaw', u'Ubersaw'),
    wordFilter(u'Artículo', u'Objeto', keepcapitalization=True),
    wordFilter(u'Artículos', u'Objetos', keepcapitalization=True),
    language='es'
)
class spanishDateFilter: # Requested by BiBi
    def __init__(self):
        self.reg = compileRegex(r'(?:Parche|Patch) del? (\d{1,2}) (?:del?)? (Enero|Febrero|Marzo|Abril|Mayo|Ju[nl]io|Agosto|Septiembre|Oct[ou]bre|Noviembre|Diciembre) de (\d{4})')
        self.filterName = u'Spanish date consistency filter'
    def replace(self, match):
        return u'Parche del ' + u(match.group(1)) + u' de ' + u(match.group(2)).title() + u' de ' + u(match.group(3))
    def __call__(self, content, **kwargs):
        return self.reg.sub(self.replace, content)
addSafeFilter(spanishDateFilter(), language='es')
addSafeFilter( # Requested by Dio, and later updated as requested by Ashe
    wordFilter(u'== Variaciones de color ==', u'== Variaciones de Colores ==', u'==+ *Variantes +pintadas *==+'), language='es'
)
addSafeFilter( # Requested by Dio
    wordFilter(u'La trampa', u'El cheto', keepcapitalization=True),
    wordFilter(u'Las trampas', u'Los chetos', keepcapitalization=True),
    wordFilter(u'Trampas', u'Chetos', keepcapitalization=True),
    wordFilter(u'Trampa', u'Cheto', keepcapitalization=True),
    language='es'
)
addSafeFilter( # Requested by Jagoba RL, then flipped by request of Dio
    wordFilter(u'SuperActualización', u'Actualizaci[oó]n [Üüu]ber'),
    language='es'
)
def spanishNivelCapitalizationFilter(t, **kwargs): # Capitalize "nivel" inside {{item infobox}} and {{backpack item}} templates, requested by rZ
    if t.getName().lower() == 'item infobox' and t.getParam('level'):
        t.setParam('level', t.getParam('level').replace('nivel', 'Nivel'))
    if t.getName().lower() == 'backpack item' and t.getParam('item-level'):
        t.setParam('item-level', t.getParam('item-level').replace('nivel', 'Nivel'))
    return t
addTemplateFilter(spanishNivelCapitalizationFilter, language='es')

French filters

enforceCapitalization(
    u'janvier', u'février', u'mars', u'avril', u'mai', u'juin', u'juillet', u'août', u'septembre', u'octobre', u'novembre', u'décembre', language='fr'
) # Do not capitalize months
addSafeFilter(wordFilter('janvier', 'janviers'), language='fr')
addSafeFilter(
    wordFilter(u'== Historique des mises à jour ==', u'==+ *(?:Historique des? mises? [aà] jour|Changements? pr.c.dents?) *==+'), language='fr'
)

Brazilian Portuguese filters

def portugueseInfoboxAvailabilityFilter(t, **kwargs): # Requested by Cructo
    if t.getName().lower() == u'item infobox':
        return t # Skip
    if t.getParam('availability'):
        t.setParam('availability', t.getParam('availability').replace(u'craft', u'fabricação').replace(u'Craft', u'Fabricação'))
    return t
addTemplateFilter(portugueseInfoboxAvailabilityFilter, language='pt-br')
addSafeFilter( # General
    wordFilter(u'Fabricação', u'Crafting', keepcapitalization=True),
    wordFilter(u'Conquista', u'Achievement', keepcapitalization=True),
    wordFilter(u'Conquistas', u'Achievements', keepcapitalization=True),
    wordFilter(u'Demomen', u'Demomans'),
    wordFilter(u'Heavies', u'Heavys'),
    wordFilter(u'N/D', u'N/A'),
    wordFilter(u'Atualização', u'Patch', keepcapitalization=True),
    wordFilter(u'Atualizações', u'Patches', keepcapitalization=True),
    wordFilter(u'Desempenho', u'Performance', keepcapitalization=True),
    wordFilter(u'Spies', u'Spys'),
    wordFilter(u'ÜberCarga', u'Sobrecarga', u'Übercharge', u'ÜberCharge'),
    wordFilter(u'Mochila', u'Backpack', u'Inventário', keepcapitalization=True),
    wordFilter(u'Comunidade Steam', u'Steam Community'),
    wordFilter(u'== Bugs ==', u'==+ *Defeitos *==+'),
    wordFilter(u'Sala de renascimento', u'Respawn room', u'Sala de respawn', keepcapitalization=True),
    wordFilter(u'Área de renascimento', u'Respawn area', u'Área de respawn',  keepcapitalization=True),
    wordFilter(u'Über Atualização', u'Atualização Über', u'Atualização do Über', u'Über Update'),
    wordFilter(u'Sistema de obtenção de itens', u'Sistema de drop de itens', u'Item drop system', u'Sistema de queda de itens', keepcapitalization=True),
    wordFilter(u'Rajada de ar', u'Compression blast', u'Airblast', keepcapitalization=True),
    wordFilter(u'minicrits', u'Mini-Crits', u'Mini-Críticos', u'Minicríticos', keepcapitalization=True),
    wordFilter(u'minicrit', u'Mini-Crit', u'Mini-Crítico', u'Minicrítico', keepcapitalization=True),
    wordFilter(u'Blog Oficial do TF2', u'TF2 Official Blog', u'Blog Oficial de TF2'),
    wordFilter(u'Personalizada', u'Customizada', keepcapitalization=True),
    wordFilter(u'Personalizado', u'Customizado', keepcapitalization=True),
    wordFilter(u'Compartimento', u'Slot', keepcapitalization=True),
    wordFilter(u'Jogabilidade', u'Gameplay', keepcapitalization=True),
    wordFilter(u'Rei do Pedaço', u'King of the Hill'),
    wordFilter(u'Disparo-alt', u'Disparo-Alt', u'Alt-Fire', keepcapitalization=True),
    wordFilter(u'Disparo alternativo', u'Fogo Alternativo', u'Disparo Alternativo', keepcapitalization=True),
    wordFilter(u'Botão de disparo-alt', u'Botão Direito do Mouse', keepcapitalization=True),
    wordFilter(u'Habilidades', u'Abilidades', keepcapitalization=True),
    wordFilter(u'Austrálio', u'Australium', keepcapitalization=True),
    wordFilter(u'Cavaleiro Carente de Cavalo e Cabeça', u'Horseless Headless Horsemann'),
    wordFilter(u'Festiva', u'Festive', keepcapitalization=True),
    wordFilter(u'Natal Australiano', u'Australian Christmas', keepcapitalization=True),
    wordFilter(u'Natal Australiano de 2011', u'Natal Australiano 2011', u'Australian Christmas 2011', keepcapitalization=True),
    wordFilter(u'== Variantes pintadas ==', u'==+ *Variantes pintados *==+', u'==+ *Variações pintadas *==+', u'==+ *Variantes pintáveis *==+'),
    wordFilter(u'== Curiosidades ==', u'==+ *Trivias? *==+'),
    wordFilter(u'Projeto', u'Planta', keepcapitalization=True),
    wordFilter(u'Projetos', u'Plantas', keepcapitalization=True),
    wordFilter(u'== Histórico de atualizações ==', u'==+ *Histórico de atualização? *==+', u'==+ *Update history? *==+'),
    wordFilter(u'pré-venda', u'pré-compra'),
    wordFilter(u'Dia das Bruxas', u'Halloween', keepcapitalization=True),
    wordFilter(u'Engine Source', u'Source Engine', u'Motor Source'),
    wordFilter(u'itens', u'items', keepcapitalization=True),
    wordFilter(u'pré-visualização', u'preview', u'previsão', keepcapitalization=True),
    wordFilter(u'pré-visualizações', u'previews', u'previsões', keepcapitalization=True),
    wordFilter(u'no Steam', u'na Steam+'),
    wordFilter(u'do Steam', u'da Steam+'),
    wordFilter(u'à Oficina Steam', u'para a Oficina Steam+'),
    wordFilter(u'cor da equipe', u'cor do time', u'cor de time', u'cor de equipe'),
    wordFilter(u'Minissentinelas', u'Minisentinelas', u'Mini-Sentinelas, keepcapitalization=True'),
    wordFilter(u'Minissentinela', u'Minisentinela', u'Mini-Sentinela, keepcapitalization=True'),
    wordFilter(u'Natal', u'Smissmas'),
    wordFilter(u'MvM', u'MVM'),
    wordFilter(u'Mann vs. Máquina', u'Mann vs Máquina', u'Mann vs. Machine', u'Mann vs Machine'),
    wordFilter(u'interface', u'HUD', keepcapitalization=True),
    wordFilter(u'captura de tela', u'screenshot', keepcapitalization=True),
    wordFilter(u'capturas de tela', u'screenshots', keepcapitalization=True),
    wordFilter(u'→', u'->', u'-->'),
    wordFilter(u'←', u'<-', u'<--'),
    wordFilter(u'conversa', u'bate-papo', u'bate papo', u'chat', keepcapitalization=True),
    wordFilter(u'on-line', u'online', keepcapitalization=True),
    wordFilter(u'off-line', u'offline', keepcapitalization=True),
    wordFilter(u'tiros fatais na cabeça', u'tiros na cabeça fatais', keepcapitalization=True),
    wordFilter(u'Loja Mann Co\.', u'Loja Mann Co\.\.', u'Loja da Mann Co\.'),
    wordFilter(u'Peça Estranha', u'parte estranha'),
    language='pt-br'
)
addSafeFilter( # Quality names
    wordFilter(u'Genuíno', u'Genuine', keepcapitalization=True),
    wordFilter(u'Feito por Mim', u'Self-Made', keepcapitalization=True),
    wordFilter(u'Normal', u'Stock', keepcapitalization=True),
    wordFilter(u'Estranho', u'Strange', keepcapitalization=True),
    wordFilter(u'Único', u'Unique', keepcapitalization=True),
    wordFilter(u'Incomum', u'Unusual', keepcapitalization=True),
    language='pt-br'
)
addSafeFilter( # Weapon names
   wordFilter(u'Espingarda', u'Scattergun'),
   wordFilter(u'Pistola', u'Pistol'),
   wordFilter(u'Taco', u'Bat'),
   wordFilter(u'Lança-Foguetes', u'Rocket Launcher'),
   wordFilter(u'Escopeta', u'Shotgun'),
   wordFilter(u'Pá', u'Shovel'),
   wordFilter(u'Lança-Chamas', u'Flame Thrower' u'Flamethrower'),
   wordFilter(u'Machado de Incêndio', u'Fire Axe'),
   wordFilter(u'Lança-Granadas', u'Grenade Launcher'),
   wordFilter(u'Lança-Stickybombs', u'Stickybomb Launcher'),
   wordFilter(u'Garrafa', u'Bottle'),
   wordFilter(u'Metralhadora Giratória', u'Minigun'),
   wordFilter(u'Punhos', u'Fists'),
   wordFilter(u'Ferramenta de Construção', u'Build PDA'),
   wordFilter(u'Ferramenta de Demolição', u'Destroy PDA'),
   wordFilter(u'Arma de Seringas', u'Syringe Gun'),
   wordFilter(u'Arma Médica', u'Medi Gun'),
   wordFilter(u'Serra de Ossos', u'Bone Saw'),
   wordFilter(u'Rifle de Precisão', u'Rifle de Sniper', u'Sniper Rifle', keepcapitalization=True),
   wordFilter(u'Submetralhadora', u'SMG'),
   wordFilter(u'Revólver', u'Revolver'),
   wordFilter(u'Faca', u'Knife'),
   wordFilter(u'Relógio de Invisibilidade', u'Invis Watch', u'Invisibility Watch'),
   wordFilter(u'Kit de Disfarce', u'Disguise Kit'),
   language='pt-br'
)
enforceCapitalization( # Do not capitalize languages, nationalities, months or days of the week (will not include abbrevs. of weekdays, as they are also ordinal numbers)
  u'inglês', u'russo', u'russa', u'francês', u'francesa', u'alemão', u'alemã', u'polonês', u'polonesa', u'brasileiro', u'brasileira', u'finlandês', u'finlandesa', u'castelhano', u'espanhol', u'espanhola', u'holandês', u'holandesa', u'chinês', u'chinesa', u'chinês simplificado', u'chinês tradicional', u'tailandês', u'tailandesa', u'ucraniano', u'ucraniana', u'árabe', u'tcheco', u'tcheca', u'dinamarquês', u'dinamarquesa', u'húngaro', u'húngara', u'italiano', u'italiana', u'japonês', u'japonesa', u'coreano', u'coreana', u'norueguês', u'norueguesa', u'português', u'portuguesa', u'romeno', u'romena', u'sueco', u'sueca', u'turco', u'egípcio', u'egípcia', u'janeiro', u'fevereiro', u'março', u'abril', u'maio', u'junho', u'julho', u'agosto', u'setembro', u'outubro', u'novembro', u'dezembro', u'segunda-feira', u'terça-feira', u'quarta-feira', u'quinta-feira', u'sexta-feira', u'sábado', u'domingo', language='pt-br'
)

Russian filters

addSafeFilter(
    regexes({
        (u'^([^<>]*)(?<!=)([""])((?:(?!\\2|[=<>]).)+)\\2([^<>]*)$', re.MULTILINE): u'$1«$3»$4',
        u'«([^»]*)(?<!\')\'([^»\']+)\'(?!\')': u'«$1„$2“'
    }),
    language='ru'
)
addSafeFilter( # Requested by FreeXMan
    wordFilter(u'Зефенайя', u'Зефанайя', u'Зефинайя'),
    wordFilter(u'Зефенайи', u'Зефанайи', u'Зефинайи'),
    wordFilter(u'Зефенайей', u'Зефанайей', u'Зефинайей'),
    wordFilter(u'Зефенай', u'Зефанай', u'Зефинай'),
    wordFilter(u'Нет', u'N/A'),
    wordFilter(u'Хеллоуин', u'Хэллоуин', keepcapitalization=True),
    wordFilter(u'Хеллоуинский', u'Хэллоуинский', keepcapitalization=True),
    wordFilter(u'Хеллоуинского', u'Хэллоуинского', keepcapitalization=True),
    wordFilter(u'Хеллоуинскому', u'Хэллоуинскому', keepcapitalization=True),
    wordFilter(u'Хеллоуинским', u'Хэллоуинским', keepcapitalization=True),
    wordFilter(u'Хеллоуинском', u'Хэллоуинском', keepcapitalization=True),
    language='ru'
)

Korean filters

addSafeFilter(dumbReplace(u'솔져', u'솔저'), language='ko') # Requested by Cyrus H.

Dutch filters

#addSafeFilter(wordFilter(u'Niveau', u'Level', keepcapitalization=True), language='nl') # Requested by Apparition; too broad
addSafeFilter( # Requested by Warlike and Heifastus
    wordFilter(u'Updateverleden', u'Updategeschiedenis', u'Update verleden', u'Update geschiedenis', keepcapitalization=True),
    wordFilter(u'Galerij', u'Gallerij', keepcapitalization=True),
    wordFilter(u'community', u'communitie', keepcapitalization=True),
    language='nl'
)
addSafeFilter( # Requested by Robin0van0der0vliet
    wordFilter(u'officieel', u'offici[eë][eë]l', keepcapitalization=True),
    wordFilter(u'officiële', u'offic[iï]ele', keepcapitalization=True),
    language='nl'
)
addSafeFilter( # Requested by Eels
    wordFilter(u'voorwerpvindsysteem', u'voorwerp vind systeem', keepcapitalization=True),
    wordFilter(u'Voorwerpschema', u'Voorwerp schema', keepcapitalization=True),
    wordFilter(u'Voorwerpschemaupdate', u'Voorwerp schema-update', u'Voorwerpschema update', keepcapitalization=True),
    language='nl'
)
addSafeFilter( # Requested by GrampaSwood
    regex(r'\b, en\b', ' en'),
    wordFilter(u'grondvoorwerpen', u'pickups', keepcapitalization=True),
    wordFilter(u'grondvoorwerp', u'pickup', keepcapitalization=True),
    wordFilter(u'bijgewerkt', u'geüpdatet', keepcapitalization=True),
    wordFilter(u'weetjes', u'trivia', keepcapitalization=True),
    language='nl'
)

Polish filters

addSafeFilter( # Requested by real_alien
    wordFilter(u'Exploit', u'Nadużycie', keepcapitalization=True),
    wordFilter(u'Exploity', u'Nadużycia', keepcapitalization=True),
    wordFilter(u'Exploitem', u'Nadużyciem', keepcapitalization=True),
    wordFilter(u'Exploitu', u'Nadużycia', keepcapitalization=True),
    language='pl'
)

Hungarian filters

addSafeFilter( # Requested by Monte
    wordFilter(u'== Frissítési előzmények ==', u'==+ ?*Frissítések ?==+', u'==+ ?*Update ?==+', u'==+ ?*Javítások ?==+'),
    wordFilter(u'== Lásd még ==', u'==+ ?See also ?==+'),
    wordFilter(u'== Források ==', u'==+ ?References ?==+'),
    wordFilter(u'== Kulisszák mögött ==', u'==+ ?Érdekességek ?==+', u'==+ ? Trivia ?==+'),
    wordFilter(u'== Festett variációk ==', u'==+ ?Painted variants ?==+'),
    wordFilter(u'== Sebzési és működési idők ==', u'==+ ?Damage and function times ?==+', u'==+ ?Sebzési? és [Ff]unkció idők ?==+'),
    wordFilter(u'=== Mint barkácsolási kellék ===', u'==+ ?As a crafting ingredient ?==+'),
    wordFilter(u'== Nem használt tartalom ==', u'==+ ?Unused content ?==+'),
    wordFilter(u'== Öszefüggő teljesítmények ==', u'==+ ?Related achievements ?==+', u'==+ ?[Aa]chievements ?==+', u'==+ ?Teljesítmények ?==+'),
    wordFilter(u'== Fura ritkaságú ==', u'==+ ?Strange variant ?==+'),
    wordFilter(u'== Tárgy-szett ==', u'==+ ?Item set ?==+', u'==+ ?Tárgyszett ?==+', u'==+ ?Tárgy szett ?==+'),
    language='hu'
)

Swedish filters

addSafeFilter( # Requested by BrazilianNut
    wordFilter(u'Ingenjörer', u'Tekniker', keepcapitalization=True),
    wordFilter(u'Ingenjören', u'Teknikern', keepcapitalization=True), 
    language='sv'
)

Link filters

tf2.com to teamfortress.com

addLinkFilter(linkDomainFilter('tf2.com', 'teamfortress.com'))

Moved links

def movedLinks(link, **kwargs):
    movedLinks = {
        u'Quality#Normal': u'Normal',
        u'Quality#Unique': u'Unique',
        u'Quality#Vintage': u'Vintage',
        u'Quality#Genuine': u'Genuine',
        u'Quality#Strange': u'Strange',
        u'Quality#Unusual': u'Unusual',
        u'Quality#Community': u'Community (quality)',
        u'Quality#Self-Made': u'Self-Made',
        u'Quality#Valve': u'Valve (quality)',
        u'vdc:Material': u'vdc:VMT'
    }
    if link.getType() == u'internal' and link.getLink() in movedLinks:
        link.setLink(movedLinks[link.getLink()])
    return link
addLinkFilter(movedLinks)

Wikipedia links filter

def wikipediaLinks(link, **kwargs):
    wikipediaRegex = compileRegex(r'^https?://(?:(\w+)\.)?wikipedia\.org/wiki/(\S+)')
    if link.getType() == u'external':
        linkInfo = wikipediaRegex.search(link.getLink())
        if linkInfo:
            link.setType(u'internal')
            try:
                wikiPage = urllib2.unquote(str(linkInfo.group(2))).decode('utf8', 'ignore').replace(u'_', ' ')
            except:
                wikiPage = u(linkInfo.group(2)).replace(u'_', ' ')
            if not linkInfo.group(1) or linkInfo.group(1).lower() == u'en':
                link.setLink(u'Wikipedia:' + wikiPage) # English Wikipedia
            else:
                link.setLink(u'Wikipedia:' + linkInfo.group(1).lower() + u':' + wikiPage) # Non-english Wikipedia
            if link.getLabel() is None:
                link.setLabel(u'(Wikipedia)')
    return link
addLinkFilter(wikipediaLinks)

Fix external wiki links that should be internal links

def fixExternalToInternalLinks(link, **kwargs):
    wikiExternalRe = compileRegex(r'^https?://wiki\.(teamfortress|tf2)\.com/wiki/(\S+)$')
    if link.getType() == 'external':
        linkMatch = wikiExternalRe.search(link.getLink())
        if linkMatch:
            link.setType('internal')
            try:
                wikiPage = u(urllib2.unquote(str(linkMatch.group(2))).decode('utf8', 'ignore').replace(u'_', ' '))
            except:
                wikiPage = u(linkMatch.group(2)).replace(u'_', ' ')
            link.setLink(wikiPage)
            if link.getLabel() is None:
                link.setLabel(wikiPage)
    return link
addLinkFilter(fixExternalToInternalLinks)

Category removal on pages using {{Item infobox}}

def removeCategory(l, **kwargs):
    catsToRemove = [u'Category:Weapons', u'Category:Hats', u'Category:Primary weapons', u'Category:Secondary weapons', u'Category:Melee weapons', u'Category:PDA1 weapons', u'Category:PDA2 weapons', u'Category:Miscellaneous items', u'Category:Tools', u'Category:Action items', u'Category:Taunts', u'Community-contributed items']
    regLang = compileRegex('/[^/]+$')
    if 'article' not in kwargs or regLang.sub(u, l.getLink()) not in catsToRemove:
        return l
    if u'Category:Item infobox usage' not in kwargs['article'].getCategories():
        return l
    return None
addLinkFilter(removeCategory)

Remove trailing slashes from internal links

def removeTrailingSlash(l, **kwargs):
    if l.getType() != u'internal' or not len(l.getLink()):
        return l
    if l.getLink()[-1] == '/':
        l.setLink(l.getLink()[:-1])
    return l
addLinkFilter(removeTrailingSlash)

Convert patch links to {{Patch name}}

def patchNameLinkFilter(l, **kwargs):
    if l.getType() != u'internal':
        return l
    regPatchName = compileRegex(u'(January|February|March|April|May|June|July|August|September|October|November|December)\\s+(\\d+),\\s+(\\d{4,})\\s+Patch(?:/\\w+)?')
    result = regPatchName.match(l.getLink())
    if result is None or l.getLabel().find(result.group(2)) == -1 or l.getLabel().find(result.group(3)) == -1:
        return l
    monthNames = ('january', 'february', 'march', 'april', 'may', 'june', 'july', 'august', 'september', 'october', 'november', 'december')
    patchType = u
    if l.getLink().lower().find(u'beta') != -1:
        patchType = u'|beta'
    elif l.getLink().lower().find(u'xbox') != -1:
        patchType = u'|xbox'
    elif l.getLink().lower().find(u'classic') != -1:
        patchType = u'|classic'
    return template(u'{{Patch name|' + u(monthNames.index(result.group(1).lower()) + 1) + u'|' + u(result.group(2)) +  u'|' + u(result.group(3)) + patchType + u'}}')
addLinkFilter(patchNameLinkFilter)

Template filters

Template renaming

def templateRenameMapping(t, **kwargs):
    templateMap = {
        # Format goes like this (without the "#" in front obviously):
        #'Good template name': ['Bad template lowercase name 1', 'Bad template lowercase name 2', 'Bad template lowercase name 3'],
        # Last line has no comma at the end
        'Scout Nav': ['scout nav/ro'],
        'Soldier Nav': ['soldier nav/ro'],
        'Pyro Nav': ['pyro nav/ro'],
        'Heavy Nav': ['heavy nav/ro'],
        'Demoman Nav': ['demoman nav/ro'],
        'Engineer Nav': ['engineer nav/ro'],
        'Medic Nav': ['medic nav/ro'],
        'Sniper Nav': ['sniper nav/ro'],
        'Spy Nav': ['spy nav/ro'],
        'Promo Nav': ['mncnav', 'pokernightnav', 'l4dnav', 'kfnav'],
        'Crush': ['pngcrush'],
        'Class infobox': ['infobox class'],
        'Video infobox': ['infobox video'],
        'Comic infobox': ['infobox comics']
    }
    for n in templateMap:
        if t.getName().lower() in templateMap[n]:
            t.setName(n)
    return t
addTemplateFilter(templateRenameMapping)

Lang template renaming

def langsTemplateRenameMapping(t, **kwargs):
    templateMap = {'Scout Nav': 'scout nav',
                   'Soldier Nav': 'soldier nav',
                   'Pyro Nav': 'pyro nav',
                   'Demoman Nav': 'demoman nav',
                   'Heavy Nav': 'heavy nav',
                   'Engineer Nav': 'engineer nav',
                   'Medic Nav': 'medic nav',
                   'Sniper Nav': 'sniper nav',
                   'Spy Nav': 'spy nav',
                   'Audio Nav': 'audionav',
                   'Video Nav': 'videonav',
                   'Class Strategy Nav': 'class strategy'
                   }
    for template in templateMap:
        langTemplateRe = compileRegex(templateMap[template] + '/(ar|cs|da|de|es|fi|fr|hu|it|ja|ko|nl|no|pl|pt|pt-br|ro|ru|sv|tr|zh-hans|zh-hant)')
        if langTemplateRe.match(t.getName().lower()):
            t.setName(template)
    return t
addTemplateFilter(langsTemplateRenameMapping)

Reindent all infoboxes

def infoboxIndentFilter(t, **kwargs):
    itemInfoboxes = ('item infobox', 'map infobox', 'item set infobox', 'mission infobox', 'class infobox', 'hazard infobox', 'pickup infobox', 'video infobox', 'comic infobox', 'website infobox', 'company infobox')
    tName = t.getName().lower()
    if 'infobox' in tName and tName not in itemInfoboxes:
        t.indentationMatters(True)
        t.setDefaultIndentation(2)
    return t
addTemplateFilter(infoboxIndentFilter, lowPriority=True)

Manage all {{Item infobox}}es

def infoboxFilter(t, **kwargs):
    filteredTemplates = ('weapon infobox', 'hat infobox', 'tool infobox', 'item infobox') # Only do stuff to these templates
    if t.getName().lower() not in filteredTemplates:
        return t # Skip
    t.setName('Item infobox')
    t.indentationMatters(True) # Reindents every time, not only when modifying values
    paramAliases = { # Parameter alias 'goodParam': 'badParam', or 'goodParam': [list of bad params].
        'name': ['weapon-name-override', 'hat-name-override', 'tool-name-override', 'NAME', 'name-override'],
        'image': ['weapon-image', 'hat-image', 'tool-image'],
        'kill-text-1': 'kill-text',
        'team-colors': 'has-team-colors',
        'two-models': 'has-two-models',
        'slot': ['weapon-slot', 'hat-slot', 'tool-slot'],
        'trade': 'tradable',
        'gift': 'giftable',
        'craft': 'craftable',
        'paint': ['paintable', 'Paint'],
        'rename': ['name-tag', 'nametag'],
        'loadout': 'display-loadout-stats',
        'level': 'level-and-type',
        'loadout-prefix': 'hide-loadout-prefix'
    }
    catstoCheck = { # Mapping 'templateAttribute': [List of 'Category|templateAttributeValue']
        'type': ['Taunts|taunt', 'Action items|action', 'Taunts|action taunt', 'Hats|hat', 'Miscellaneous items|misc item', 'Tools|tools', 'Weapons|weapon'],
        'slot': ['Primary weapons|primary', 'Secondary weapons|secondary', 'Melee weapons|melee', 'PDA1 weapons|pda 1', 'PDA2 weapons|pda 2']
    }
    catsDontCheck = [u'Category:Beta and unused content', 'Category:Taunts'] # Type and slot won't be modified on these pages
    preferedOrder = [ # Prefered order of keys inside template
        'name', 'game', 'type', 'beta', 'unused', 'image', 'imagewidth', '3d-team', '3d-alt', '3d-team-alt', '3d-image-#', '3d-button-#', '3d-viewname-#', 'number-of-3d-images', 'number-of-3d-team-images', 'number-of-3d-alt-images', 'view#', 'view#name', 'team-colors', 'team-colors-class#', 'team-colors-class#-name', 'team-colors-width', 'two-models', 'skin-image-red', 'skin-image-blu', 'tfc-model', 'tfc-model-3d-image-#', 'tfc-model-3d-viewname-#', 'tfc-model-3d-button-#', 'qtf-model', 'hide-kill-icon', 'kill-icon-#', 'kill-text-#', 'kill-tooltip-#', 'used-by', 'slot', 'crafting-slot', 'custom-slot', 'equip-region', 'equip-region-#', 'weapon-script', 'contributed-by', 'released', 'released-major', 'availability', 'trade', 'gift', 'marketable', 'craft', 'paint', 'rename', 'numbered', 'medieval', 'ammo-loaded', 'ammo-carried', 'ammo-type', 'show-ammo', 'reload', 'loadout', 'loadout-prefix', 'prefix', 'suffix', 'quality', '%ATTRIBUTES%', 'item-kind', 'item-level', 'level', 'paint-color', 'decal-icon', 'unusual-icon', 'stat-icon', 'strange-icon', 'pyroland-icon', 'halloween-icon', 'limited', 'unusual-effect', 'grade', 'wear', 'item-description', 'item-uses', 'item-flags', 'item-expiration'
    ]
    checkEnglish = ['trade', 'gift', 'marketable', 'craft', 'paint', 'rename', 'numbered', 'medieval', 'contributed-by', 'equip-region', 'equip-region-#'] # If these attributes aren't yes or no, check the values on the english page
    attributeTypes = ['neutral', 'positive', 'negative'] # Possible loadout attribute types
    regLang = compileRegex('/[^/]+$')
    # Step 0 - Check categories:
    tCats = None
    isTFC = False # Assume false by default
    if 'article' in kwargs:
        if kwargs['article'] is not None:
            cats2 = kwargs['article'].getCategories()
            tCats = []
            for c in cats2:
                tCats.append(u(regLang.sub(u'', u(c))))
            isTFC = u'Category:Weapons (Classic)' in tCats
    # Step 1 - Rename obsolete attributes
    for p in paramAliases:
        if type(paramAliases[p]) is type([]):
            for a in paramAliases[p]:
                t.renameParam(a, p)
        else:
            t.renameParam(paramAliases[p], p)
    # Step 2 - Fix ammo stuff, delete pricing attributes
    t.delParam('ammo', 'price', 'show-price', 'purchasable', 'backpack-image')
    # Step 3 - Fix reload stuff
    if t.getParam('reload') is None:
        t.renameParam('reload-type', 'reload')
    # Step 4 - Count, split, order and fix loadout attributes
    attrNumber = 1
    regexAttrSplit = compileRegex(r'\s*<br[^<>]*>\s*')
    for a in attributeTypes:
        if t.getParam(a + '-attributes') is not None:
            attrs = regexAttrSplit.split(t.getParam(a + '-attributes'))
            for attr in attrs:
                t.setParam('att-' + str(attrNumber) + '-' + a, attr)
                attrNumber += 1
            t.delParam(a + '-attributes')
    if tCats is not None:
        # Step 5 - Lookup english fallback on certain attributes
        fetchEnglish = False
        values = {}
        for attr in checkEnglish:
            if t.getParam(attr) is not None and t.getParam(attr).lower() not in (u'yes', u'no'):
                fetchEnglish = True
            elif t.getParam(attr) is not None:
                values[attr] = t.getParam(attr).lower()
        if regLang.search(kwargs['article'].title):
            englishArticle = page(regLang.sub(u'', kwargs['article'].title))
            try:
                englishContent = englishArticle.getWikiText()
            except:
                englishContent = u
            englishContent, englishTemplates, englishKeys = templateExtract(englishContent)
            for englishT in englishTemplates.values():
                if englishT.getName().lower() in filteredTemplates:
                    for p in paramAliases:
                        if type(paramAliases[p]) is type([]):
                            for a in paramAliases[p]:
                                englishT.renameParam(a, p)
                        else:
                            englishT.renameParam(paramAliases[p], p)
                    for attr in checkEnglish:
                        if englishT.getParam(attr) is not None:
                            values[attr] = templateRestore(englishT.getParam(attr), englishTemplates, englishKeys)
                    break
        for attr in values:
            t.setParam(attr, values[attr])
        checkCats = True
        for c in catsDontCheck:
            if c in tCats:
                checkCats = False
                break
        if not isTFC and not t.getParam('custom-slot') and checkCats:
            # Step 6 - Set certains attributes based on page categories
            for cname in catstoCheck:
                cname = u(cname)
                found = None
                foundmultiple = False
                for c in catstoCheck[cname]:
                    cat, val = u(c).split(u'|')
                    cat = u'Category:' + u(regLang.sub(u'', cat))
                    if cat in tCats:
                        if found is not None:
                            foundmultiple = True
                        found = val
                if not foundmultiple:
                    pass#t.setParam(cname, found) # Temporarily disabled for Made Man
            if u'Category:Weapons' not in tCats:
                t.delParam('slot')
            # Step 7 - Convert neutral attributes - Disabled
            """desc = []
            if t.getParam('item-description') is not None:
                desc = regexAttrSplit.split(t.getParam('item-description'))
            for i in range(1, 10):
                if t.getParam('att-' + str(i) + '-neutral') is not None:
                    desc.append(t.getParam('att-' + str(i) + '-neutral'))
                    t.delParam('att-' + str(i) + '-neutral')
            desc2 = []
            for d in desc:
                if d.strip():
                    desc2.append(d.strip())
            if len(desc2):
                t.setParam('item-description', u'
'.join(desc2))""" # Step 7.5 TEMPORARY: Add numbered = no if not isTFC and not t.getParam('numbered'): t.setParam('numbered', 'no') # Step 8 - Do TFC stuff if isTFC: t.setParam('type', 'weapon') t.setParam('game', 'tfc') # Step 9 - Set correct preferred indentation for k in ['quality', 'item-level', 'level', 'item-description', 'item-uses', 'item-flags', 'level-and-type', 'loadout-name', 'loadout-prefix', 'hide-loadout-prefix', 'limited', 'grade', 'wear', 'unusual-effect', 'item-kind', 'prefix', 'suffix', 'decal-icon', 'unusual-icon', 'stat-icon', 'strange-icon', 'pyroland-icon', 'halloween-icon', 'custom-icon', 'killcount', 'rankson', 'rankson2', 'rankson3', 'rankson4', 'rankson5', 'rankson6', 'rankson7', 'rankson8', 'rankson9', 'item-expiration']: t.setPreferedIndentation(k, 2) for i in range(1, 10): for a in attributeTypes: t.setPreferedIndentation('att-' + str(i) + '-' + a, 2) # Step 10 - Build correct attribute order newOrder = [] for o in preferedOrder: if o.find('#') == -1: newOrder.append(o) continue if o == '%ATTRIBUTES%': for i in range(1, 10): for a in attributeTypes: newOrder.append('att-' + str(i) + '-' + a) for i in range(1, 30): newOrder.append(o.replace('#', str(i))) t.setPreferedOrder(newOrder) # Step 11 - replace deprecated hat/misc types if t.getParam('type') is not None and t.getParam('type').lower() in (u'hat', u'hats', u'head', u'headwear', u'misc item', u'misc.', u'misc', u'miscellaneous', u'miscellaneous item'): t.setParam('type', 'cosmetic') # Step 12 - There is no step 12 return t addTemplateFilter(infoboxFilter)

Remove useless templates

def removeUselessTemplate(t, **kwargs):
    if t.getName().lower() in (u'targeted', u'languages', u'auto lang cat'):
        return None # Delete template
    return t
addTemplateFilter(removeUselessTemplate)

Replace Wikipedia link template with interwiki link

def replaceWikipediaTemplate(t, **kwargs):
    if 'article' not in kwargs:
        return t
    if t.getName().lower() != 'w':
        return t
    link = u'[[w:'
    if t.getParam('lang'):
        link += t.getParam('lang') + u':'
    link += t.getParam('1') + u'|'
    if t.getParam('2'):
        link += t.getParam('2')
    else:
        link += t.getParam('1')
    link += u']]'
    return link
addTemplateFilter(replaceWikipediaTemplate, lowPriority=True)

Remove manual video IDs from {{Weapon Demonstration}}

def weaponDemonstrationFilter(t, **kwargs):
    if t.getName().lower() != 'weapon demonstration':
        return t # Skip
    t.delParam('1')
    return t
addTemplateFilter(weaponDemonstrationFilter)

Filter parameters of certain templates

def templateParamFilter(t, **kwargs):
    params = { # Map: 'lowercase template name': ['list', 'of', 'params', 'to', 'filter']
        'patch layout': ['before', 'after', 'current'],
        'item infobox': ['released'],
        'update history': [1],
        'otherwikis': [1, 2]
    }
    if t.getName().lower() not in params:
        return t
    for p in params[t.getName().lower()]:
        if t.getParam(p):
            t.setParam(p, fixContent(t.getParam(p), **kwargs))
    return t
addTemplateFilter(templateParamFilter)

Remove obsolete parameters

def obsoleteParameterFilter(t, **kwargs):
    params = { # Map: 'lowercase template name': ['list', 'of', 'params', 'to', 'delete']
        'blueprint': ['ingredient-#n-local', 'result-local', 'result-#n-local'],
        'taunt': ['weapon-#n-local'],
        'patch layout': ['diff-#n'],
        'item infobox': ['crafting-slot'],
        'model info': ['location'],
        'map infobox': ['map-stamp-link']
    }
    if t.getName().lower() not in params:
        return t
    for p in params[t.getName().lower()]:
        p = u(p)
        if p.find(u'#n') != -1:
            for i in range(10):
                t.delParam(p.replace(u'#n', str(i)))
        else:
            t.delParam(p)
    return t
addTemplateFilter(obsoleteParameterFilter, lowPriority=True)

Add day/month/year to {{Patch layout}}

def patchLayoutFilter(t, **kwargs):
    if t.getName().lower() != 'patch layout' or 'article' not in kwargs:
        return t
    t.setPreferedOrder(['game', 'before', 'day', 'month', 'year', 'after', 'source-title', 'source', 'source-lang'] + [['source-' + str(n) + '-title', 'source-' + str(n), 'source-' + str(n) + '-lang'] for n in xrange(10)] + ['updatelink', 'update', 'update-link', 'update-lang', 'hide-diff'] + [['diff-' + str(n)] for n in xrange(10)] + ['notes'])
    t.delParam('current')
    regPatchName = compileRegex(u'^(January|February|March|April|May|June|July|August|September|October|November|December)\\s+(\\d+),\\s+(\\d{4,})\\s+Patch(?:/\\w+)?')
    result = regPatchName.match(u(kwargs['article'].title))
    if result is not None:
        t.setParam('day', result.group(2))
        t.setParam('month', result.group(1).lower())
        t.setParam('year', result.group(3))
    return t
addTemplateFilter(patchLayoutFilter)

Implement {{Dictionary}}

class DictionaryUpdater:
    def __init__(self):
        self.subpageTemplateLang = """{{#switch:{{{lang|{{SUBPAGENAME}}}}}|%options%}}"""
        self.subpageTemplateParam = """{{#switch:{{{1|}}}|%options%}}"""
        self.invalidParamError = """<span class="error">Error: invalid param.</span>[[Category:ERROR]]"""
        self.invalidKeyNameCharacters = """#<>[]{}"""
        self.subpageTemplateID = """%string%"""
        self.partialUpdateThreshold = 750 # Update SyncData every n edits
        self.dictionaries = {
            u'Template:Dictionary/items': { # Dictionary page
                'name': 'items', # Dictionary name (used for categorizing)
                'sync': 'Template:Dictionary/items/Special:SyncData' # Page holding last sync data
            },
            u'Template:Dictionary/common strings': { # Warning: no underscore
                'name': 'common strings',
                'sync': 'Template:Dictionary/common strings/Special:SyncData'
            },
            u'Template:Dictionary/classes': {
                'name': 'classes',
                'sync': 'Template:Dictionary/classes/Special:SyncData'
            },
            u'Template:Dictionary/demonstrations': {
                'name': 'demonstrations',
                'sync': 'Template:Dictionary/demonstrations/Special:SyncData'
            },
            u'Template:Dictionary/price': {
                'name': 'price',
                'sync': 'Template:Dictionary/price/Special:SyncData',
                'allTemplate': '{{{{{template|item price/fmt}}}|%options%|tt={{{tt|yes}}}}}'
            },
            u'Template:Dictionary/merchandise': {
                'name': 'merchandise',
                'sync': 'Template:Dictionary/merchandise/Special:SyncData'
            },
            u'Template:Dictionary/decorated': {
                'name': 'decorated',
                'sync': 'Template:Dictionary/decorated/Special:SyncData'
            },
            u'Template:Dictionary/descriptions': {
                'name': 'descriptions',
                'sync': 'Template:Dictionary/descriptions/Special:SyncData'
            },
            u'Template:Dictionary/dyk': {
                'name': 'dyk',
                'sync': 'Template:Dictionary/dyk/Special:SyncData'
            },
            u'Template:Dictionary/tournament medals': {
                'name': 'tournament medals',
                'sync': 'Template:Dictionary/tournament medals/Special:SyncData'
            },
            u'Template:Dictionary/attributes': {
                'name': 'attributes',
                'sync': 'Template:Dictionary/attributes/Special:SyncData'
            },
            u'Template:Dictionary/steam ids': {
                'name': 'steam ids',
                'sync': 'Template:Dictionary/steam ids/Special:SyncData'
            },
            u'Template:Dictionary/achievements/scout': {
                'name': 'achievements/scout',
                'sync': 'Template:Dictionary/achievements/scout/Special:SyncData'
            },
            u'Template:Dictionary/achievements/soldier': {
                'name': 'achievements/soldier',
                'sync': 'Template:Dictionary/achievements/soldier/Special:SyncData'
            },
            u'Template:Dictionary/achievements/pyro': {
                'name': 'achievements/pyro',
                'sync': 'Template:Dictionary/achievements/pyro/Special:SyncData'
            },
            u'Template:Dictionary/achievements/demoman': {
                'name': 'achievements/demoman',
                'sync': 'Template:Dictionary/achievements/demoman/Special:SyncData'
            },
            u'Template:Dictionary/achievements/heavy': {
                'name': 'achievements/heavy',
                'sync': 'Template:Dictionary/achievements/heavy/Special:SyncData'
            },
            u'Template:Dictionary/achievements/engineer': {
                'name': 'achievements/engineer',
                'sync': 'Template:Dictionary/achievements/engineer/Special:SyncData'
            },
            u'Template:Dictionary/achievements/medic': {
                'name': 'achievements/medic',
                'sync': 'Template:Dictionary/achievements/medic/Special:SyncData'
            },
            u'Template:Dictionary/achievements/sniper': {
                'name': 'achievements/sniper',
                'sync': 'Template:Dictionary/achievements/sniper/Special:SyncData'
            },
            u'Template:Dictionary/achievements/spy': {
                'name': 'achievements/spy',
                'sync': 'Template:Dictionary/achievements/spy/Special:SyncData'
            },
            u'Template:Dictionary/achievements/general': {
                'name': 'achievements/general',
                'sync': 'Template:Dictionary/achievements/general/Special:SyncData'
            },
            u'Template:Dictionary/achievements/halloween': {
                'name': 'achievements/halloween',
                'sync': 'Template:Dictionary/achievements/halloween/Special:SyncData'
            },
            u'Template:Dictionary/achievements/treasure hunt': {
                'name': 'achievements/treasure hunt',
                'sync': 'Template:Dictionary/achievements/treasure hunt/Special:SyncData'
            },
            u'Template:Dictionary/achievements/replay': {
                'name': 'achievements/replay',
                'sync': 'Template:Dictionary/achievements/replay/Special:SyncData'
            },
            u'Template:Dictionary/achievements/summer camp': {
                'name': 'achievements/summer camp',
                'sync': 'Template:Dictionary/achievements/summer camp/Special:SyncData'
            },
            u'Template:Dictionary/achievements/foundry': {
                'name': 'achievements/foundry',
                'sync': 'Template:Dictionary/achievements/foundry/Special:SyncData'
            },
            u'Template:Dictionary/achievements/christmas': {
                'name': 'achievements/christmas',
                'sync': 'Template:Dictionary/achievements/christmas/Special:SyncData'
            },
            u'Template:Dictionary/achievements/astro-chievements': {
                'name': 'achievements/astro-chievements',
                'sync': 'Template:Dictionary/achievements/astro-chievements/Special:SyncData'
            },
            u'Template:Dictionary/achievements/mann vs. machievements': {
                'name': 'achievements/mann vs. machievements',
                'sync': 'Template:Dictionary/achievements/mann vs. machievements/Special:SyncData'
            },
            u'Template:Dictionary/achievements/standin': {
                'name': 'achievements/standin',
                'sync': 'Template:Dictionary/achievements/standin/Special:SyncData'
            },
            u'Template:Dictionary/achievements/process': {
                'name': 'achievements/process',
                'sync': 'Template:Dictionary/achievements/process/Special:SyncData'
            },
            u'Template:Dictionary/achievements/snakewater': {
                'name': 'achievements/snakewater',
                'sync': 'Template:Dictionary/achievements/snakewater/Special:SyncData'
            },
            u'Template:Dictionary/achievements/powerhouse': {
                'name': 'achievements/powerhouse',
                'sync': 'Template:Dictionary/achievements/powerhouse/Special:SyncData'
            },
            u'Template:Dictionary/achievements/other games': {
                'name': 'achievements/other games',
                'sync': 'Template:Dictionary/achievements/other games/Special:SyncData'
            },
            u'Template:Dictionary/achievements/pass time': {
                'name': 'achievements/pass time',
                'sync': 'Template:Dictionary/achievements/pass time/Special:SyncData'
            },
            u'Template:Dictionary/blueprints': {
                'name': 'blueprints',
                'sync': 'Template:Dictionary/blueprints/Special:SyncData'
            },
            u'Template:Dictionary/defindex': {
                'name': 'defindex',
                'sync': 'Template:Dictionary/defindex/Special:SyncData'
            },
            u'Template:Dictionary/quad': {
                'name': 'quad',
                'sync': 'Template:Dictionary/quad/Special:SyncData',
                'blankString': '-'
            },
            u'Template:Dictionary/gameinfo': {
                'name': 'gameinfo',
                'sync': 'Template:Dictionary/gameinfo/Special:SyncData'
            }
        }
        self.subpageSeparator = u'/'
        # List of supported languages, in prefered order
        self.languages = [u'en', u'ar', u'bg', u'cs', u'da', u'de', u'es', u'fi', u'fr', u'hu', u'it', u'ja', u'ko', u'nl', u'no', u'pl', u'pt', u'pt-br', u'ro', u'ru', u'sv', u'th', u'tr', u'uk', u'vi', u'zh-hans', u'zh-hant']
        self.defaultLang = u'en'
        self.allKeyName = u'_all_'
        self.filterName = u'Your friendly neighborhood dictionary updater'
        self.commentsExtract = compileRegex(r)
        self.subkeyName = compileRegex(r'^([-\w]+)$', re.IGNORECASE)
        addWhitelistPage(self.dictionaries.keys())
        self.editCounts = {}
    def updateSyncData(self, currentDict, syncData, note=):
        # Build syncdata string representation
        syncKeys = syncData.keys()
        syncKeys.sort()
        syncLines = []
        for k in syncKeys:
            syncLines.append(k + u':' + syncData[k])
        if note:
            note = u' (' + u(note) + u')'
        editPage(self.dictionaries[currentDict]['sync'], u'\n'.join(syncLines), summary=u'Updated synchronization information for [[:' + currentDict + u']]' + note + u'.', minor=True, nocreate=False)
    def generateSubpage(self, keyName, data, currentDict, syncData):
        h = hashlib.md5()
        if type(data) is type({}): # Subkeys (translations or not)
            isTranslation = True
            subpage = u(self.subpageTemplateLang)
            for k in data:
                if 'blankString' in self.dictionaries[currentDict] and data[k] == self.dictionaries[currentDict]['blankString']:
                    data[k] = u
                if isTranslation and k not in self.languages:
                    isTranslation = False
                    subpage = u(self.subpageTemplateParam)
            ordered = []
            unordered = {}
            if isTranslation:
                missing = []
                for lang in self.languages:
                    if lang in data:
                        ordered.append(lang + u'=' + data[lang])
                        unordered[lang] = data[lang]
                        h.update((lang + u'=' + data[lang]).encode('utf8'))
                    else:
                        missing.append(lang)
                        h.update((u'null-' + lang).encode('utf8'))
                if self.defaultLang in data:
                    ordered.insert(0, u'#default=' + data[self.defaultLang])
                if len(missing):
                    subpage = subpage.replace(u'%missing%', u"Languages missing: " + u', '.join(missing))
                else:
                    subpage = subpage.replace(u'%missing%', u"Supported languages: all")
            else: # Not a translation
                h.update('Any-')
                subkeys = data.keys()
                subkeys.sort()
                for k in subkeys:
                    ordered.append(k + u'=' + data[k])
                    unordered[k] = data[k]
                    h.update((k + u'=' + data[k]).encode('utf8'))
            if 'allTemplate' in self.dictionaries[currentDict] and (len(unordered) or len(self.dictionaries[currentDict]['allTemplate']['params'])):
                allKey = []
                keys = unordered.keys()
                keys.sort()
                for k in keys:
                    allKey.append(k + u'=' + unordered[k])
                insertIndex = 0
                if isTranslation and self.defaultLang in data:
                    insertIndex = 1
                ordered.insert(insertIndex, u(self.allKeyName) + u'=' + u(self.dictionaries[currentDict]['allTemplate'].replace(u'%options%', u'|'.join(allKey))))
            subpage = subpage.replace(u'%options%', u'|'.join(ordered))
        else: # No subkeys
            data = u(data)
            subpage = self.subpageTemplateID
            h.update(u(u'ID-' + data).encode('utf8'))
            subpage = subpage.replace(u'%string%', data)
        h = u(h.hexdigest())
        if keyName in syncData and syncData[keyName] == h:
            return # Same hash
        subpage = subpage.replace(u'%dictionary%', currentDict)
        subpage = subpage.replace(u'%dictionaryname%', self.dictionaries[currentDict]['name'])
        subpage = subpage.replace(u'%keyname%', keyName)
        if editPage(currentDict + self.subpageSeparator + keyName, subpage, summary=u'Pushed changes from [[:' + currentDict + u']] for string "' + keyName + u'".', minor=True, nocreate=False):
            syncData[keyName] = h # Update sync data
            if currentDict not in self.editCounts:
                self.editCounts[currentDict] = 0
            self.editCounts[currentDict] += 1
            if self.editCounts[currentDict] > self.partialUpdateThreshold:
                self.editCounts[currentDict] = 0
                self.updateSyncData(currentDict, syncData, 'Partial update')
    def dedup(self, l):
        s = set()
        for i in l:
            if i not in s:
                s.add(i)
                yield i
    def processComment(self, commentString, currentDict, definedStrings, syncData):
        commentContents = []
        commentString = u(commentString).replace(u'\r', u)
        parseState = {
            'currentKeys': [],
            'currentSubkeys': {},
            'currentKeyIsValid': False,
            'abortCurrentSubkeys': False,
            'subKeyLines': {},
        }
        subPageData = {}
        def finalize():
            if parseState['currentKeyIsValid']:
                # End processing of current set of subkeys.
                if not parseState['abortCurrentSubkeys']:
                    isTranslation = True
                    for k in parseState['currentKeys']:
                        assert k not in subPageData, 'Internal logic consistency error: duplicate key %r' % (k,)
                        subPageData[k] = {}
                        for subKey, data in parseState['currentSubkeys'].items():
                            isTranslation = isTranslation and subKey in self.languages
                            subPageData[k][subKey] = data
                    if isTranslation:
                        for lang in self.languages:
                            if lang in parseState['subKeyLines']:
                                commentContents.append(parseState['subKeyLines'][lang])
                    else:
                        for subKey in sorted(parseState['subKeyLines'].keys()):
                            commentContents.append(parseState['subKeyLines'][subKey])
                    parseState['abortCurrentSubkeys'] = False
                parseState['currentKeyIsValid'] = False
        for line in commentString.split(u'\n'):
            if u'WINDBOT_INVALID' in line:
                commentContents.append(line)
                continue
            def badLine(reason):
                if parseState['abortCurrentSubkeys']:
                    for k in sorted(parseState['subKeyLines'].keys()):
                        commentContents.append(parseState['subKeyLines'][k] + u'  // WINDBOT_INVALID Other subkeys for this key are invalid')
                    parseState['subKeyLines'] = {}
                if parseState['currentKeyIsValid'] and not parseState['abortCurrentSubkeys']:
                    parseState['abortCurrentSubkeys'] = True
                else:
                    parseState['currentKeyIsValid'] = False
                commentContents.append(line + u'  // WINDBOT_INVALID ' + u(reason.replace(u':', ' ')))
            if line.strip() == u:
                finalize()
                commentContents.append(line)
                continue
            if line.strip()[0] == u'#':  # Human comment
                commentContents.append(line)
                continue
            if line[0] not in (u' ', '\t'):  # Key, or key + no-subkey data
                if line.find(u':') == -1:  # Colon was probably forgotten
                    badLine('Maybe a forgotten colon?')
                    continue
                if parseState['currentKeyIsValid']:
                    badLine('Missing linebreak before new key?')
                    continue
                beforeColon, afterColon = line.split(u':', 1)
                afterColon = afterColon.strip()
                # Check keys.
                keyNames = [k.replace(u'_', u' ').replace(u'#', u).strip().lower() for k in beforeColon.strip().split(u'|')]
                keyNames = [k for k in self.dedup(keyNames) if k]
                if len(keyNames) == 0:
                    badLine('No valid key names')
                    continue
                duplicateKey = None
                badKey = None
                for k in keyNames:
                    for c in self.invalidKeyNameCharacters:
                        if c in k:
                            badKey = k
                            break
                    if k in definedStrings:
                        duplicateKey = k
                        break
                    if k in subPageData:
                        duplicateKey = k
                        break
                if duplicateKey:
                    badLine('Duplicate key: %r' % duplicateKey)
                    continue
                if badKey:
                    badLine('Key has invalid characters: %r' % badKey)
                    continue
                # Key looks good.
                for k in keyNames:
                    definedStrings.add(k)
                # Check for no-subkey data.
                if afterColon:
                    for k in keyNames:
                        subPageData[k] = afterColon
                    commentContents.append(u' | '.join(keyNames) + u': ' + afterColon)
                else:
                    parseState['currentKeyIsValid'] = True
                    parseState['abortCurrentSubkeys'] = False
                    parseState['currentKeys'] = keyNames
                    parseState['currentSubkeys'] = {}
                    parseState['subKeyLines'] = {}
                    commentContents.append(u' | '.join(keyNames) + u':')
                continue
            # Sub-key definition follows (has a space as first character).
            if parseState['abortCurrentSubkeys']:
                commentContents.append(line)
                continue
            if not parseState['currentKeyIsValid']:
                badLine('Sub-key being defined despite no valid key')
                continue
            if line.find(u':') == -1:  # Colon was probably forgotten
                badLine('Missing colon in subkey definition')
                continue
            beforeColon, afterColon = line.strip().split(u':', 1)
            subKeyNames = [k.strip().lower() for k in beforeColon.strip().split(u'|')]
            subKeyNames = [k for k in self.dedup(subKeyNames) if k]
            badSubkeyName = None
            duplicateSubkey = None
            for k in subKeyNames:
                if not self.subkeyName.match(k):
                    badSubkeyName = k
                    break
                if k in parseState['currentSubkeys'].keys():
                    duplicateSubkey = k
                    break
            if badSubkeyName:
                badLine('Invalid subkey name: %r' % badSubkeyName)
                continue
            if duplicateSubkey:
                badLine('Duplicate subkey name: %r' % duplicateSubkey)
                continue
            subKeyData = afterColon.strip()
            if not subKeyData:
                badLine('Empty data')
                continue
            for k in subKeyNames:
                parseState['currentSubkeys'][k] = subKeyData
                parseState['subKeyLines'][k] = u'  ' + k + u': ' + subKeyData
        finalize()
        for k, data in subPageData.items():
            self.generateSubpage(k, data, currentDict, syncData)
        return u'\n'.join(commentContents)
    def __call__(self, content, **kwargs):
        if 'article' not in kwargs:
            return content
        if u(kwargs['article'].title) not in self.dictionaries:
            return content
        currentDict = u(kwargs['article'].title)
        if random.randint(0, 50) == 0: # With probability 2%, ignore syncdata completely. Helps with stale syncdata and people overwriting things.
            syncDataText = u
        else:
            try:
                syncDataText = u(page(self.dictionaries[currentDict]['sync']).getWikiText()).split(u'\n')
            except: # Page probably doesn't exist
                syncDataText = u
        syncData = {}
        for sync in syncDataText:
            sync = u(sync.strip())
            if not sync:
                continue
            sync = sync.split(u':', 1)
            if len(sync) == 2:
                syncData[sync[0]] = sync[1]
        oldSyncData = syncData.copy()
        newContent = u
        previousIndex = 0
        definedStrings = set()
        for comment in self.commentsExtract.finditer(content):
            newContent += content[previousIndex:comment.start()]
            previousIndex = comment.end()
            # Process current comment
            newContent += u'<!--\n\n' + self.processComment(u(comment.group(1)).strip(), currentDict, definedStrings, syncData) + u'\n\n-->'
        newContent += content[previousIndex:]
        # Check for deleted strings
        for k in oldSyncData:
            if k not in definedStrings:
                try:
                    deletePage(currentDict + self.subpageSeparator + k, 'Removed deleted string "' + k + u'" from ' + currentDict + u'.')
                except:
                    pass
                if k in syncData:
                    del syncData[k]
        self.updateSyncData(currentDict, syncData, 'Full update')
        self.editCounts[currentDict] = 0
        return newContent
    def scheduledRun(self):
        for d in self.dictionaries:
            fixPage(d)
dictUpdater = DictionaryUpdater()
addFilter(dictUpdater)
scheduleTask(dictUpdater.scheduledRun, 3)

Remove some templates on translated pages if they are absent on english page

def autoRemoveRecentAdditionTag(t, **kwargs):
    # "stub" is included here because translators often copy this template from the English page
    # instead of replacing it with "trans" or "update trans".
    # This wiki is English first, and as such, stub usage in translated pages is incorrect.
    # In the future, it might be more useful to just replace "stub" with "trans" or "update trans" instead.
    # - T 01/08/24
    toRemoveTemplates = ('recent addition', 'ra', 'new item', 'unreleased', 'beta', 'stub')
    equivalences = {
        'ra': 'recent addition',
        'new item': 'recent addition'
    }
    if t.getName().lower() not in toRemoveTemplates or 'article' not in kwargs or '/' not in kwargs['article'].title:
        return t
    targetTemplate = t.getName().lower()
    if targetTemplate in equivalences:
        targetTemplate = equivalences[targetTemplate]
    englishArticle = page(kwargs['article'].title[:kwargs['article'].title.find('/')])
    try:
        englishContent = englishArticle.getWikiText()
        englishContent, englishTemplates, englishKeys = templateExtract(englishContent)
        for t2 in englishTemplates.values():
            if t2.getName().lower() in toRemoveTemplates:
                foundTemplate = t2.getName().lower()
                if foundTemplate in equivalences:
                    foundTemplate = equivalences[foundTemplate]
                if foundTemplate == targetTemplate:
                    return t
        return None
    except:
        return t
addTemplateFilter(autoRemoveRecentAdditionTag)

File filters

Crush all PNG/JPG images

Delegated to CrushBOT.

Additional tasks

Update {{Dictionary/defindex}} and {{Dictionary/price}}

TEMPORARILY DISABLED: WindBOT can't seem to properly fetch the item schema (probably an issue in steamGetGameSchema()), which causes this function to fail.

PhoneWave is taking care of this for now. See User:PhoneWave/forceDictionary/defindex.

def updateGameDictionaries():
	if steam is None:
		return
	# Begin configurable section
	game = 440
	priceDictionaryPage = u'Template:Dictionary/price'
	indexDictionaryPage = u'Template:Dictionary/defindex'
	itemEquivalences = {
		u'taunt: the (.*)': u'\\1',
		u'taunt: (.*)': u'\\1',
		u'taunt: the (.*)': u'\\1 (taunt)', # Useful for Meet the Medic
		u'taunt: (.*)': u'\\1 (taunt)', # Useful for Meet the Medic
		u'strange filter: (.*)': u'strange filter - \\1'
	}
	forceDefindex = {
		u'champ stamp': 815,
		u'confidential collection case': 5823,
		u'fall 2013 acorns crate key': 5710,
		u'flying guillotine': 812,
		u'gargoyle key': 5827,
		u'gloves of running urgently': 239,
		u'human cannonball': 817,
		u'huo-long heater': 811,
		u'lugermorph': 160,
		u'mann co. stockpile crate': 5738,
		u'mann co. stockpile crate key': 5740,
		u'mann co. store package': 729,
		u'mann co. strongbox key': 5720,
		u'mann co. supply crate key': 5021,
		u'marxman': 816,
		u'neon annihilator': 813,
		u'pda': 737,
		u'pyrovision goggles': 743,
		u'quarantined collection case': 5822,
		u'red-tape recorder': 810,
		u'something special for someone special': 5074,
		u'triad trinket': 814,
		u'what\'s in the team fortress 2 soundtrack box?': 1176,
		u'love and war cosmetics bundle': 2131,
	}
	decimalSeparator = '{{dec}}'
	illegalItemCharacters = compileRegex(r'[:%#\r\n\t]+')
	priceDictionaryHeader = u'{{dictionary/header}}\n== /price ==\n\'\'\'This dictionary is automatically updated by WindBOT\'\'\'. Edits made to this page will be overwritten.\n<!--\n'
	indexDictionaryHeader = u'{{dictionary/header}}\n== /defindex ==\n\'\'\'This dictionary is automatically updated by WindBOT\'\'\'. Edits made to this page will be overwritten.\n<!--\n'
	dictionaryFooter = u'\n-->'
	# End configurable section
	schema = steamGetGameSchema(game)
	assets = steamGetGameAssets(game)
	dictionary = {}
	defindex = {}
	for item in schema:
		itemName = u(item.name).lower().replace(u'_', u' ')
		keys = [itemName]
		for r in itemEquivalences:
			itemName = re.sub(u'^' + r + u'$', itemEquivalences[r], itemName)
			if itemName not in keys:
				keys.append(itemName)
		itemKeys = u' | '.join([illegalItemCharacters.sub(u'', x) for x in keys])
		defindex[itemName] = itemKeys + u': ' + u(item.schema_id)
		if item not in assets:
			continue
		prices = assets[item.schema_id].price
		if not len(prices):
			continue
		prices = dict((u(k).strip().lower(), v) for k, v in prices.iteritems()) # Sanitize data
		dictionary[itemName] = itemKeys + u':'
		for currency in sorted(prices):
			price = prices[currency]
			if price.is_integer():
				price = u(int(price))
			else:
				price = u(round(price, 2))
				dot = price.find(u'.')
				if dot == len(price) - 2:
					price += u'0'
			dictionary[itemName] += u'\n  ' + u(currency).lower() + u': ' + price.replace('.', decimalSeparator)
	for item in forceDefindex:
		defindex[u(item)] = illegalItemCharacters.sub(u'', u(item)) + u': ' + u(forceDefindex[item])
	if len(dictionary):
		# Is updating time doktor
		finalPage = priceDictionaryHeader
		for item in sorted(dictionary):
			finalPage += u'\n' + dictionary[item] + u'\n'
		finalPage += dictionaryFooter
		priceDictionaryPage = page(priceDictionaryPage)
		editPage(priceDictionaryPage, finalPage, summary=u'Updated item prices from WebAPI.', minor=True, bot=True, nocreate=True)
		# Run other filters on the new page
		fixPage(priceDictionaryPage)
	if len(defindex):
		# Is also updating time doktor
		finalPage = indexDictionaryHeader
		for item in sorted(defindex):
			finalPage += u'\n' + defindex[item] + u'\n'
		finalPage += dictionaryFooter
		indexDictionaryPage = page(indexDictionaryPage)
		editPage(indexDictionaryPage, finalPage, summary=u'Updated item indexes from WebAPI.', minor=True, bot=True, nocreate=True)
		# Run other filters on the new page
		fixPage(indexDictionaryPage)
# scheduleTask(updateGameDictionaries, 2)

Update Lastpatch and Lastpatchbeta

# Redirect Updater Class - modified from http://github.com/i-ghost/wikiscripts
# To update the lastpatch redirects on Team Fortress Wiki, but could probably be deployed on The Portal Wiki/Dota 2 Wiki
# i-ghost
	
class redirectUpdater(object):
	def __init__(self, updateTemplateName="Template:Updates", pageName="Lastpatch", betaPageName="Lastpatchbeta"):
		self.pageName = pageName
		self.betaPageName = betaPageName
		self.updateTemplateName = updateTemplateName
		self.pageText = page(self.pageName).getWikiText()
		self.betaPageText = page(self.betaPageName).getWikiText()
		self.updateTemplateText = page(self.updateTemplateName).getWikiText().split("\n")
		self.langs = ["ru", "fr", "de", "pl", "pt-br", "fi", "es", "nl", "zh-hans", "zh-hant", "ar", "cs", "da", "hu", "it", "ja", "ko", "no", "pt", "ro", "sv", "tr"]
		self._get_dates()
		self.patch = "%s %s, %s" % (self._get_month(int(self.updates["patch-month"])), self.updates["patch-day"], self.updates["patch-year"])
		self.betaPatch = "%s %s, %s" % (self._get_month(int(self.updates["patch-beta-month"])), self.updates["patch-beta-day"], self.updates["patch-beta-year"])
		self.footer = "<!-- This page is automatically generated when %s is modifed. Do not modify this page. -->" % (self.updateTemplateName)
		
	def _get_month(self, month):
		"""WindBOT doesn't import calendar, so we use this instead"""
		return datetime.date(1960, month, 1).strftime("%B")
		
	def _get_dates(self):
		"""Internal: Gets dates from update template and stores in dictionary"""
		self.updates = {}
		for i in self.updateTemplateText:
			if i.find("|") == 0:
				# Liable to break
				self.updates[i.lstrip("|").replace(" ", "").partition("=")[0]] = i.replace(" ", "").partition("=")[2].replace("<!--Don'tforgettoupdateme!-->", "")
			
	def make_edit_strings(self, beta=False, lang=False):
		"""Creates the final page content."""
		if beta and lang:
 			self.redirect_string = "#REDIRECT [[%s Patch (Beta)/%s]] {{R lang|%s}}" % (self.betaPatch, lang, lang)
 			self.summary_string = "Updated [[%s/%s]] redirect to [[%s Patch (Beta)/%s]]" % (self.betaPageName, lang, self.betaPatch, lang)
 		elif beta and not lang:
 			self.redirect_string = "#REDIRECT [[%s Patch (Beta)]]" % (self.betaPatch)
 			self.summary_string = "Updated [[%s]] redirect to [[%s Patch (Beta)]]" % (self.betaPageName, self.betaPatch)
 			
 		if lang and not beta:
 			self.redirect_string = "#REDIRECT [[%s Patch/%s]] {{R lang|%s}}" % (self.patch, lang, lang)
 			self.summary_string = "Updated [[%s/%s]] redirect to [[%s Patch/%s]]" % (self.pageName, lang, self.patch, lang)
 		elif not lang and not beta:
 			self.redirect_string = "#REDIRECT [[%s Patch]]" % (self.patch)
 			self.summary_string = "Updated [[%s]] redirect to [[%s Patch]]" % (self.pageName, self.patch)
		
	def _update_redirect(self, beta=False, lang=False):
		"""Internal: Provides editing functionality"""
		self.make_edit_strings(beta, lang)
		# Use the correct page
		if beta:
			if lang:
				_pagetoedit = "%s/%s" % (self.betaPageName, lang)
			else:
				_pagetoedit = self.betaPageName
		else:
			if lang:
				_pagetoedit = "%s/%s" % (self.pageName, lang)
			else:
				_pagetoedit = self.pageName
		# Send the edit
		editPage(_pagetoedit, "%s\n%s" % (self.redirect_string, self.footer), summary=self.summary_string, minor=True, nocreate=False)
			
	def check_if_update_needed(self, beta=False):
		"""Checks if redirects needs updating"""
		try:
			if beta:
				if self.betaPageText.split("[[")[1].partition("]]")[0].rstrip("Patch (Beta)") != self.betaPatch:    return True
			else:
				if self.pageText.split("[[")[1].partition("]]")[0].rstrip(" Patch") != self.patch:    return True
		except IndexError:
			return True # Just update anyway
			
	def update(self, beta=False):
		"""Updates the redirects and their lang pages"""
		if beta:
			self._update_redirect(beta=True)
			for lang in self.langs:
				self._update_redirect(beta=True, lang=lang)
		else:
			self._update_redirect()
			for lang in self.langs:
				self._update_redirect(lang=lang)

	def run(self):
		"""Runs everything"""
		# Beta
		if self.check_if_update_needed(beta=True):
			self.update(beta=True)
		# Normal
		if self.check_if_update_needed():
			self.update()

scheduleTask(redirectUpdater().run, 2)

Update game prices

Delegated to PhoneWave. See User:PhoneWave/forceDictionary/gameinfo for gameids.

Update {{Dictionary/steam ids}}

def updatePlayerInfoDictionary(): # By Smashman
    """A WindBOT filter created to fetch Steam user's name and vanity information and store it in Template:Dictionary/steam_ids"""
    # Begin configurable section
    playerInfoDictionaryHeader = u'{{dictionary/header}}\n== /steam_ids ==\n\'\'\'The information in this dictionary is automatically updated by WindBOT. Add new Steam IDs in [[Template:Dictionary/steam ids/id list]]\'\'\'.\n<!--\n'
     playerListDictionaryHeader = u'{{dictionary/header}}\n== /steam_ids/id_list ==\n\'\'\'The information in this dictionary is tracked by WindBOT. Add new Steam IDs to the bottom of this list\'\'\'.\n\n'
     playerInfoDictionaryFooter = u'\n-->'
    # End configurable section
    # Is updating time doktor
    playerInfoDictionaryPage = u'Template:Dictionary/steam ids'
    playerListDictionaryPage = u'Template:Dictionary/steam ids/id list'
    id_list = page('Template:Dictionary/steam ids/id list').getWikiText()
    id64s=sorted(frozenset(filter(lambda x: x.isdigit(), id_list.splitlines())))
    listPage = playerListDictionaryHeader + '\n'.join(map(str, id64s)) + '\n'
    editPage(playerListDictionaryPage, listPage, summary=u'Sort tracked Steam IDs.', minor=True, bot=True, nocreate=True)
    start_limit = 0
    end_limit = 100
    i=0
    div_100 = (len(id64s)/100)+1 #Times to repeat sections
    finalPage = playerInfoDictionaryHeader
    while i != div_100:
        id64_sort = sorted(id64s[start_limit:end_limit]) #Numerically sort the ids, for use in call
        id64s_csv = ','.join(id64_sort) #Put them into a csv string
        data = steam.api.interface("ISteamUser").GetPlayerSummaries(version = 2, steamids = id64s_csv) #Do the call
        data = sorted(data["response"]["players"], key=lambda x: x['steamid']) #Numerically sort by id
        for player in data:
            purl = player["profileurl"].strip('/')
            finalPage += u"\n{0}:".format(player["steamid"])
            finalPage += u("\n  nickname: <no" + u"wiki>{0}</no" + u"wiki>").format(player["personaname"].replace(u'<!--', u'<!--').replace(u'-->', u'-->'))
            if purl.find("/id/") != -1:
                vanity = os.path.basename(purl)
                finalPage += u"\n  vanity: {0}".format(vanity)
            finalPage += u"\n"
        start_limit += 100
        end_limit += 100
        i+=1
    finalPage += playerInfoDictionaryFooter
    editPage(playerInfoDictionaryPage, finalPage, summary=u'Updated steam player information from WebAPI.', minor=True, bot=True, nocreate=True)
    fixPage(playerInfoDictionaryPage)
scheduleTask(updatePlayerInfoDictionary, 16)
def updateDictionaryOnSteamIdListUpdate(content, **kwargs):
    if 'article' in kwargs and kwargs['article'] == 'Template:Dictionary/steam ids/id list':
        updatePlayerInfoDictionary()
    return content
addFilter(updateDictionaryOnSteamIdListUpdate)