Greetings,
I've taken the above Python script to make it general where there will be a prompt for the word list and the lexicon if there is one and output an SMF file to import into FLEx
#!/usr/bin/python3
"""
wordlist_to_sfm.py
Exports a Paratext word list to SFM (Standard Format Markers) format
for import into FLEx (FieldWorks Language Explorer).
Usage:
python wordlist_to_sfm.py
The script will prompt for:
- A Paratext word list XML file (e.g. exported from Paratext's word list tool)
- Optionally, a Paratext Lexicon XML file to supply English glosses
- An output file path for the resulting .sfm file
Only words marked as "Correct" in the word list are exported by default.
This can be changed by editing the INCLUDE_* settings below.
Output format is MDF (Multi-Dictionary Formatter), the standard SFM dialect used by FLEx for lexicon import.
"""
import codecs
import os
import xml.etree.ElementTree as etree
# --- Settings ----------------------------------------------------------------
# Control which spelling categories are included in the export.
# Set to True to include words in that category, False to exclude them.
INCLUDE_CORRECT = True # Words the translator has approved
INCLUDE_UNKNOWN = False # Words not yet reviewed
INCLUDE_INCORRECT = False # Words flagged as misspellings
# Set to True to write the word's corpus frequency as a comment field (\co_count)
INCLUDE_COUNT = True
# -----------------------------------------------------------------------------
def prompt_path(prompt_text, default):
"""Show a prompt with an optional default value; return the user's input or the default."""
display = f" [{default}]" if default else ""
value = input(f"{prompt_text}{display}: ").strip()
return value if value else default
def get_user_inputs():
"""Ask the user for the three file paths needed to run the export."""
print("Paratext Word List to SFM Exporter")
print("-----------------------------------\n")
# Word list is required - keep asking until the file is found
wordlist_path = prompt_path("Word list file (XML)", "Notsi-WL.xml")
while not os.path.exists(wordlist_path):
print(f" Cannot find '{wordlist_path}'. Please check the path and try again.")
wordlist_path = prompt_path("Word list file (XML)", "Notsi-WL.xml")
# Lexicon is optional - skip silently if not found
lexicon_path = prompt_path("Lexicon file for glosses (press Enter to skip)", "")
if lexicon_path and not os.path.exists(lexicon_path):
print(f" Cannot find '{lexicon_path}'. Continuing without glosses.")
lexicon_path = ""
output_path = prompt_path("Output file name", "wordlist_for_flex.sfm")
return wordlist_path, lexicon_path, output_path
def load_lexicon_glosses(lexicon_path):
"""
Read English glosses from a Paratext Lexicon XML file.
Returns a dictionary keyed by lexeme form, where each value contains the lexeme type (Word, Prefix, Suffix) and a list of English gloss strings.
Returns an empty dictionary if no lexicon path is provided.
"""
glosses = {}
if not lexicon_path:
print("No lexicon provided - entries will be exported without glosses.")
return glosses
print(f"Reading lexicon from: {lexicon_path}")
with open(lexicon_path, "rb") as f:
data = f.read()
# Strip any BOM variants (standard UTF-8 BOM, or double-encoded BOM)
for bom in (b"\xef\xbb\xbf", b"\xc3\xaf\xc2\xbb\xc2\xbf"):
if data.startswith(bom):
data = data[len(bom):]
break
xmldoc = etree.fromstring(data)
for item in xmldoc.findall("Entries/item"):
lexeme = next(item.iter("Lexeme"), None)
if lexeme is None:
continue
form = lexeme.get("Form", "")
lex_type = lexeme.get("Type", "Word") # Word, Prefix, or Suffix
# Collect all English glosses for this entry (there may be more than one sense)
entry_glosses = [
gloss.text
for gloss in item.iter("Gloss")
if gloss.get("Language") == "en" and gloss.text
]
if form and entry_glosses:
glosses[form] = {"type": lex_type, "glosses": entry_glosses}
print(f" Found {len(glosses)} entries with English glosses.")
return glosses
def build_approved_list(wordlist_path):
"""
Read the Paratext word list XML and return words that pass the INCLUDE_* filters.
Returns a list of approved word forms and a metadata dictionary containing each word's corpus count, hyphenation, and morphology breakdown.
"""
wordlist = etree.parse(wordlist_path)
all_items = wordlist.getroot().findall("item")
print(f"Reading word list from: {wordlist_path}")
print(f" {len(all_items)} words found in list.")
approved = []
metadata = {}
for item in all_items:
spelling = item.attrib.get("spelling", "Unknown")
word = item.attrib.get("word", "")
include = (
(spelling == "Correct" and INCLUDE_CORRECT) or
(spelling == "Unknown" and INCLUDE_UNKNOWN) or
(spelling == "Incorrect" and INCLUDE_INCORRECT)
)
if include and word:
approved.append(word)
metadata[word] = {
"count": item.attrib.get("count", "0"),
"hyphenation": item.attrib.get("hyphenation", ""),
"morphology": item.attrib.get("morphology", ""),
"morph_approved": item.attrib.get("morphologyApproved", "False"),
"specificcase": item.attrib.get("specificcase", ""),
}
print(f" {len(approved)} words marked as correct and ready to export.")
return approved, metadata
def write_sfm_entry(outfile, word, lex_type, glosses, meta, include_count):
"""
Write a single SFM lexicon entry to the output file.
MDF conventions followed:
\\lx - lexeme (headword); affixes get a hyphen on the bound side
\\sn - sense number (written once per gloss)
\\ge - English gloss for that sense
\\mr - morphology string (when approved by the translator)
\\co_* - comment fields for metadata FLEx doesn't have a standard marker for
"""
# Write the headword, adding hyphens to show affix attachment points
outfile.write("\n\\lx ")
if lex_type == "Suffix":
outfile.write("-")
outfile.write(word)
if lex_type == "Prefix":
outfile.write("-")
outfile.write("\n")
# Record affix type as a comment so it survives the FLEx import
if lex_type != "Word":
outfile.write(f"\\co_type {lex_type}\n")
# Corpus frequency - useful for prioritising entries during dictionary work
if include_count and meta:
outfile.write(f"\\co_count {meta['count']}\n")
# Morphology: use the standard \\mr marker if the analysis has been approved,
# otherwise store it as a comment to avoid importing unverified data
if meta and meta["morphology"]:
marker = "\\mr" if meta["morph_approved"] == "True" else "\\co_morph"
outfile.write(f"{marker} {meta['morphology']}\n")
# Write one sense block per gloss
for sense_num, gloss_text in enumerate(glosses, start=1):
outfile.write(f"\\sn {sense_num}\n")
outfile.write(f"\\ge {gloss_text}\n")
def main():
"""
Main export routine.
Two-pass approach:
Pass 1 - write entries that have lexicon glosses (richer data first)
Pass 2 - write remaining approved words that had no lexicon entry
This ensures glossed entries are not duplicated.
"""
wordlist_path, lexicon_path, output_path = get_user_inputs()
print()
approved_words, metadata = build_approved_list(wordlist_path)
lexicon_glosses = load_lexicon_glosses(lexicon_path)
# Open output file as UTF-8 and write the MDF header
outfile = codecs.open(output_path, mode="w", encoding="utf-8")
outfile.write("\\_sh v3.0 400 MDF\n\\_DateStampHasFourDigitYear\n\n")
remaining = list(approved_words) # Words still to be written after pass 1
with_glosses = 0
# Pass 1: entries that appear in both the lexicon and the approved word list
for form, lex_data in lexicon_glosses.items():
if form not in approved_words:
print(f" Skipping '{form}' (in lexicon but not marked as correct in word list)")
continue
if form in remaining:
remaining.remove(form)
write_sfm_entry(outfile, word=form, lex_type=lex_data["type"],
glosses=lex_data["glosses"], meta=metadata.get(form),
include_count=INCLUDE_COUNT)
with_glosses += 1
# Pass 2: approved words with no lexicon entry - exported without glosses
for word in remaining:
write_sfm_entry(outfile, word=word, lex_type="Word", glosses=[],
meta=metadata.get(word), include_count=INCLUDE_COUNT)
total = with_glosses + len(remaining)
outfile.close()
print(f"\nExport complete.")
print(f" Total entries written : {total}")
print(f" With glosses : {with_glosses}")
print(f" Without glosses : {len(remaining)}")
print(f" Output file : {output_path}")
if __name__ == "__main__":
main()