Source code for flexicon.code.System.WritingSystemOperations

#
#   WritingSystemOperations.py
#
#   Class: WritingSystemOperations
#          Writing system management operations for FieldWorks Language Explorer
#          projects via SIL Language and Culture Model (LCM) API.
#
#   Platform: Python.NET
#             FieldWorks Version 9+
#
#   Copyright 2025
#

import logging

# Import FLEx LCM types
from SIL.LCModel import SpecialWritingSystemCodes
from SIL.LCModel.Core.KernelInterfaces import ITsString
from SIL.LCModel.Core.Text import TsStringUtils
from SIL.WritingSystems import WritingSystemDefinition  # Fixed: was IWritingSystemDefinition

# Import flexlibs exceptions
from ..FLExProject import (
    FP_ParameterError,
    FP_WritingSystemError,
)
from ..BaseOperations import BaseOperations, OperationsMethod, wrap_enumerable

logger = logging.getLogger(__name__)


[docs] class WritingSystemOperations(BaseOperations): """ This class provides operations for managing writing systems in a FieldWorks project. Writing systems define how text is displayed, including language, script, font settings, and directionality (left-to-right or right-to-left). This class should be accessed via FLExProject.WritingSystems property. Usage:: from flexicon import FLExProject project = FLExProject() project.OpenProject("my project", writeEnabled=True) # Get all writing systems for ws in project.WritingSystems.GetAll(): name = project.WritingSystems.GetDisplayName(ws) tag = project.WritingSystems.GetLanguageTag(ws) print(f"{name} ({tag})") # Get vernacular and analysis writing systems vern_wss = project.WritingSystems.GetVernacular() anal_wss = project.WritingSystems.GetAnalysis() # Configure a writing system ws = list(vern_wss)[0] project.WritingSystems.SetFontName(ws, "Charis SIL") project.WritingSystems.SetFontSize(ws, 12) project.WritingSystems.SetRightToLeft(ws, False) # Check and set defaults default_vern = project.WritingSystems.GetDefaultVernacular() default_anal = project.WritingSystems.GetDefaultAnalysis() project.CloseProject() """ def __init__(self, project): """ Initialize WritingSystemOperations with a FLExProject instance. Args: project: The FLExProject instance to operate on. """ super().__init__(project) # --- Core CRUD Operations --- @wrap_enumerable @OperationsMethod def GetAll(self): """ Get all active writing systems in the project. Returns all writing systems that are currently active (both vernacular and analysis) in the project. Returns: EnumerableWrapper[IWritingSystemDefinition]: Each writing system object Example: >>> for ws in project.WritingSystems.GetAll(): ... name = project.WritingSystems.GetDisplayName(ws) ... tag = project.WritingSystems.GetLanguageTag(ws) ... font = project.WritingSystems.GetFontName(ws) ... print(f"{name} ({tag}): {font}") English (en): Calibri French (fr): Arial Vernacular (qaa-x-kal): Charis SIL Notes: - Returns only writing systems that are active in the project - Includes both vernacular and analysis writing systems - Use GetVernacular() or GetAnalysis() to filter by type See Also: GetVernacular, GetAnalysis, Exists """ vern_ws_set = self._GetAllVernacularWSTags() anal_ws_set = self._GetAllAnalysisWSTags() active_tags = vern_ws_set | anal_ws_set for ws in self.project.project.ServiceLocator.WritingSystems.AllWritingSystems: if ws.Id in active_tags: yield ws @wrap_enumerable @OperationsMethod def GetVernacular(self): """ Get all vernacular writing systems. Vernacular writing systems are those used for the language(s) being studied or documented. Yields: IWritingSystemDefinition: Each vernacular writing system Example: >>> for ws in project.WritingSystems.GetVernacular(): ... name = project.WritingSystems.GetDisplayName(ws) ... tag = project.WritingSystems.GetLanguageTag(ws) ... print(f"Vernacular: {name} ({tag})") Vernacular: Kalaba (qaa-x-kal) Vernacular: Kalaba-IPA (qaa-x-kal-fonipa) Notes: - Vernacular writing systems are for the object language - These are the primary languages of the lexicon entries - Most projects have 1-3 vernacular writing systems See Also: GetAnalysis, GetDefaultVernacular, SetDefaultVernacular """ vern_ws_set = self._GetAllVernacularWSTags() for ws in self.project.project.ServiceLocator.WritingSystems.AllWritingSystems: if ws.Id in vern_ws_set: yield ws @wrap_enumerable @OperationsMethod def GetAnalysis(self): """ Get all analysis writing systems. Analysis writing systems are those used for linguistic analysis, glosses, and translations (typically major languages like English, French, Spanish, etc.). Yields: IWritingSystemDefinition: Each analysis writing system Example: >>> for ws in project.WritingSystems.GetAnalysis(): ... name = project.WritingSystems.GetDisplayName(ws) ... tag = project.WritingSystems.GetLanguageTag(ws) ... print(f"Analysis: {name} ({tag})") Analysis: English (en) Analysis: French (fr) Analysis: Spanish (es) Notes: - Analysis writing systems are for metadata languages - Used for glosses, definitions, and translations - Most projects have 1-3 analysis writing systems See Also: GetVernacular, GetDefaultAnalysis, SetDefaultAnalysis """ anal_ws_set = self._GetAllAnalysisWSTags() for ws in self.project.project.ServiceLocator.WritingSystems.AllWritingSystems: if ws.Id in anal_ws_set: yield ws @OperationsMethod def Create(self, language_tag, name, is_vernacular=True): """ Create (or activate) a writing system in the project. Args: language_tag (str): Language tag (e.g., "en", "fr", "qaa-x-kal") name (str): Display name for the writing system. Only used when a brand-new LDML is created -- ignored when ``language_tag`` is already present in the project's LDML store (see Notes). is_vernacular (bool): True for vernacular, False for analysis. Defaults to True. Returns: IWritingSystemDefinition: The newly created or newly activated writing system Raises: FP_ReadOnlyError: If project is not opened with write enabled FP_NullParameterError: If language_tag or name is None FP_ParameterError: If language_tag is empty, or already active (vernacular or analysis) -- see ``Exists()`` Example: >>> # Create a vernacular writing system >>> ws = project.WritingSystems.Create("qaa-x-kal", "Kalaba") >>> print(project.WritingSystems.GetDisplayName(ws)) Kalaba >>> # Create an analysis writing system >>> ws = project.WritingSystems.Create("es", "Spanish", is_vernacular=False) >>> project.WritingSystems.SetFontName(ws, "Arial") >>> project.WritingSystems.SetFontSize(ws, 12) >>> # Create IPA writing system >>> ws = project.WritingSystems.Create("qaa-x-kal-fonipa", "Kalaba IPA") >>> project.WritingSystems.SetFontName(ws, "Charis SIL") Notes: - Language tags should follow BCP 47 standard - Use "qaa-x-" prefix for undocumented languages - The writing system is automatically added to vernacular or analysis list based on is_vernacular parameter - Default font settings may be inherited from system defaults - **Store-present-but-inactive tags (issue #250 Defect 2):** the guard above only refuses an already-ACTIVE tag (what ``Exists()`` checks). If ``language_tag`` already has an LDML in the project's store -- e.g. a writing system that was configured once and later deactivated, or inherited from a template (``ExistsInStore()`` would return True) -- this method does NOT create a second, duplicate ``WritingSystemDefinition`` for it. It reuses the existing one and only activates it (adds it to the requested current list); ``name`` is ignored in that case since the writing system's DisplayLabel already exists. Use ``Ensure()`` if you want a single idempotent call that also tells you which of these two paths was taken. See Also: Delete, Exists, ExistsInStore, Ensure, GetAll """ self._EnsureWriteEnabled() self._ValidateParam(language_tag, "language_tag") self._ValidateParam(name, "name") if not language_tag or not language_tag.strip(): raise FP_ParameterError("Language tag cannot be empty") # Check if writing system is already ACTIVE (vernacular or # analysis). Exists() answers only that question (issue #250 # Defect 1) -- a store-present-but-inactive tag does NOT raise # here; it is handled below by reusing rather than re-creating. if self.Exists(language_tag): raise FP_ParameterError(f"Writing system '{language_tag}' already exists") ws_manager = self.project.project.ServiceLocator.WritingSystemManager with self._TransactionCM(f"Create writing system '{language_tag}'"): # A tag can be present in the project's LDML store without being # active (issue #250 Defect 2) -- store-presence and usability # are different questions, and conflating them previously made # this method the ONLY route to AddToCurrent*WritingSystems # refuse to activate a store-present-but-inactive tag at all. # _GetWSByTag() is the whole-store lookup (what ExistsInStore() # exposes publicly); reuse the existing definition instead of # asking the manager to create a duplicate for an Id it already # tracks. existing_ws = self._GetWSByTag(language_tag) if existing_ws is not None: ws = existing_ws else: # Genuinely new: create + register the WS with the manager. # Set() stores it in the repository, assigns a runtime # Handle, and persists the .ldml backing file on Save. # Without Set(), Create() returns a detached object -- # adding the tag to CurXxxWss would yield an orphan # reference and the next OpenProject would hit a modal # "Unable to create writing system: <tag>" dialog from # liblcm. ws = ws_manager.Create(language_tag) ws.Abbreviation = language_tag try: ws.DisplayLabel = name except Exception: pass ws_manager.Set(ws) # AddToCurrent* updates BOTH the full XxxWss list AND the current # list via the proper change-notification path. Raw assignment to # CurXxxWss (the previous implementation) bypassed the WS-list # collection bookkeeping and left the full list out of sync. if is_vernacular: self.project.lp.AddToCurrentVernacularWritingSystems(ws) else: self.project.lp.AddToCurrentAnalysisWritingSystems(ws) return ws @OperationsMethod def Ensure(self, language_tag, name, is_vernacular=True): """ Idempotently ensure a writing system is active, creating it only if it does not exist anywhere in the project yet. Resolves the ``Exists()``/``Create()`` deadlock at the heart of issue #250 (Defects 1-3): a caller previously had no single-call way to ask "make this writing system usable" without first arbitrating between two collections that disagree (``Exists()``'s active-only answer vs. ``Create()``'s own now-fixed store-presence check). ``Ensure()`` collapses that into one idempotent call. Args: language_tag (str): Language tag (e.g., "en", "fr", "qaa-x-kal") name (str): Display name, used ONLY when a genuinely new writing system is created. Ignored when the tag is already active or already present in the store (its existing DisplayLabel is left untouched). is_vernacular (bool): True to ensure/activate as a vernacular writing system, False for analysis. Defaults to True. If the tag is already active in the OTHER category, it is ADDED to the requested category as well rather than moved -- FLEx allows a writing system to be both vernacular and analysis simultaneously, and ``Ensure()`` never demotes an existing activation. Returns: tuple[IWritingSystemDefinition, bool]: ``(ws, created)``. ``created`` is True only when a genuinely new writing system was created (the tag did not exist anywhere in the project's LDML store before this call). It is False both when the tag was already active (a true no-op) and when it was present in the store but inactive and has now been activated -- both leave the store's writing-system count unchanged, which is the operationally relevant distinction for an idempotent pre-pass such as a cross-project sync guard. See Notes for how to observe the finer three-way distinction the issue also asks for. Raises: FP_ReadOnlyError: If project is not opened with write enabled FP_NullParameterError: If language_tag or name is None FP_ParameterError: If language_tag is empty Example: >>> # Replaces the old, incorrect Exists()-then-Create() dance: >>> # if not tgt_ops.Exists(tag): # False positive risk >>> # tgt_ops.Create(tag, name) # would then raise >>> ws, created = project.WritingSystems.Ensure("etu-fonipa", "Ejagham IPA") >>> created False # was present in the store but inactive; now activated Notes: - Three input states are possible (already-active, present-but-inactive, genuinely-absent) but ``created`` distinguishes only two outcomes (see Returns). All three are logged at INFO level to ``logging.getLogger(__name__)`` so a caller that needs the fuller distinction can read it from the log, or can call ``Exists()`` before ``Ensure()`` if it needs to know synchronously which case applied. - Never removes or deactivates a writing system; only adds. See Also: Exists, ExistsInStore, Create """ self._ValidateParam(language_tag, "language_tag") self._ValidateParam(name, "name") self._EnsureWriteEnabled() if not language_tag or not language_tag.strip(): raise FP_ParameterError("Language tag cannot be empty") target_tags = ( self._GetAllVernacularWSTags() if is_vernacular else self._GetAllAnalysisWSTags() ) existing_ws = self._GetWSByTag(language_tag) # whole-store lookup if existing_ws is not None and existing_ws.Id in target_tags: logger.info( "WritingSystems.Ensure(%r): already active (%s); no-op.", language_tag, "vernacular" if is_vernacular else "analysis", ) return existing_ws, False ws_manager = self.project.project.ServiceLocator.WritingSystemManager with self._TransactionCM(f"Ensure writing system '{language_tag}'"): if existing_ws is not None: ws = existing_ws created = False logger.info( "WritingSystems.Ensure(%r): present in store but " "inactive; activating without creating a new LDML.", language_tag, ) else: ws = ws_manager.Create(language_tag) ws.Abbreviation = language_tag try: ws.DisplayLabel = name except Exception: pass ws_manager.Set(ws) created = True logger.info( "WritingSystems.Ensure(%r): not present anywhere; " "creating a new writing system.", language_tag, ) if is_vernacular: self.project.lp.AddToCurrentVernacularWritingSystems(ws) else: self.project.lp.AddToCurrentAnalysisWritingSystems(ws) return ws, created @OperationsMethod def Delete(self, ws_handle_or_tag): """ Remove a writing system from the project. Args: ws_handle_or_tag: Either a writing system handle (int) or language tag (str) Raises: FP_ReadOnlyError: If project is not opened with write enabled FP_NullParameterError: If ws_handle_or_tag is None FP_WritingSystemError: If writing system not found FP_ParameterError: If trying to delete the default WS Example: >>> # Delete by language tag >>> if project.WritingSystems.Exists("qaa-x-old"): ... project.WritingSystems.Delete("qaa-x-old") >>> # Delete by handle >>> ws = list(project.WritingSystems.GetVernacular())[2] >>> handle = ws.Handle >>> project.WritingSystems.Delete(handle) Notes: - Cannot delete default vernacular or analysis writing systems - Should check that no data uses this writing system before deleting - This operation cannot be undone - Writing system is removed from both the store and active lists See Also: Create, Exists """ self._EnsureWriteEnabled() self._ValidateParam(ws_handle_or_tag, "ws_handle_or_tag") # Get the writing system object if isinstance(ws_handle_or_tag, str): language_tag = ws_handle_or_tag ws = self._GetWSByTag(language_tag) if not ws: raise FP_WritingSystemError(language_tag) else: ws = self._GetWSByHandle(ws_handle_or_tag) if not ws: raise FP_WritingSystemError(str(ws_handle_or_tag)) language_tag = ws.Id # Check if it's a default writing system default_vern = self.project.lp.DefaultVernacularWritingSystem default_anal = self.project.lp.DefaultAnalysisWritingSystem if ws.Handle == default_vern.Handle: raise FP_ParameterError("Cannot delete the default vernacular writing system") if ws.Handle == default_anal.Handle: raise FP_ParameterError("Cannot delete the default analysis writing system") with self._TransactionCM(f"Delete writing system '{language_tag}'"): # Removal contract from IWritingSystemContainer: "first remove from # AnalysisWritingSystems [the full list], then from # CurrentAnalysisWritingSystems [the current list]." Same for # vernacular. Going through the collections (rather than # rewriting the CurXxxWss space-delimited string) keeps the full # list and current list in sync. for full_list, current_list in ( (self.project.lp.VernacularWritingSystems, self.project.lp.CurrentVernacularWritingSystems), (self.project.lp.AnalysisWritingSystems, self.project.lp.CurrentAnalysisWritingSystems), ): if full_list.Contains(ws): full_list.Remove(ws) if current_list.Contains(ws): current_list.Remove(ws) # --- Configuration Methods --- @OperationsMethod def GetFontName(self, ws): """ Get the default font name for a writing system. Args: ws: IWritingSystemDefinition object or language tag (str) Returns: str: Font name (e.g., "Charis SIL", "Arial") Raises: FP_NullParameterError: If ws is None FP_WritingSystemError: If writing system not found Example: >>> ws = list(project.WritingSystems.GetVernacular())[0] >>> font = project.WritingSystems.GetFontName(ws) >>> print(f"Font: {font}") Font: Charis SIL >>> # By language tag >>> font = project.WritingSystems.GetFontName("en") >>> print(font) Calibri See Also: SetFontName, GetFontSize """ ws_obj = self._ResolveWS(ws) if not ws_obj: raise FP_WritingSystemError(str(ws)) # Get default font name if hasattr(ws_obj, "DefaultFontName") and ws_obj.DefaultFontName: return ws_obj.DefaultFontName elif hasattr(ws_obj, "DefaultFont") and ws_obj.DefaultFont: return ws_obj.DefaultFont else: return "" @OperationsMethod def SetFontName(self, ws, font_name): """ Set the default font name for a writing system. Args: ws: IWritingSystemDefinition object or language tag (str) font_name (str): Font name (e.g., "Charis SIL", "Arial") Raises: FP_ReadOnlyError: If project is not opened with write enabled FP_NullParameterError: If ws or font_name is None FP_WritingSystemError: If writing system not found Example: >>> ws = list(project.WritingSystems.GetVernacular())[0] >>> project.WritingSystems.SetFontName(ws, "Charis SIL") >>> # Set by language tag >>> project.WritingSystems.SetFontName("en", "Calibri") >>> # Set IPA font >>> project.WritingSystems.SetFontName("qaa-x-kal-fonipa", "Doulos SIL") Notes: - Font must be installed on the system - Changes affect all text displayed in this writing system - Recommended fonts for IPA: Charis SIL, Doulos SIL See Also: GetFontName, SetFontSize """ self._EnsureWriteEnabled() self._ValidateParam(font_name, "font_name") ws_obj = self._ResolveWS(ws) if not ws_obj: raise FP_WritingSystemError(str(ws)) # Set default font name. The hasattr dispatch stays OUTSIDE the # brackets so the "neither property exists" fall-through remains a # true no-op that opens no unit of work. if hasattr(ws_obj, "DefaultFontName"): with self._TransactionCM(f"Set writing system font '{font_name}'"): ws_obj.DefaultFontName = font_name elif hasattr(ws_obj, "DefaultFont"): with self._TransactionCM(f"Set writing system font '{font_name}'"): ws_obj.DefaultFont = font_name @OperationsMethod def GetFontSize(self, ws): """ Get the default font size for a writing system. Args: ws: IWritingSystemDefinition object or language tag (str) Returns: float: Font size in points (e.g., 12.0) Raises: FP_NullParameterError: If ws is None FP_WritingSystemError: If writing system not found Example: >>> ws = list(project.WritingSystems.GetVernacular())[0] >>> size = project.WritingSystems.GetFontSize(ws) >>> print(f"Font size: {size} pt") Font size: 12.0 pt See Also: SetFontSize, GetFontName """ ws_obj = self._ResolveWS(ws) if not ws_obj: raise FP_WritingSystemError(str(ws)) # Get default font size if hasattr(ws_obj, "DefaultFontSize") and ws_obj.DefaultFontSize: return float(ws_obj.DefaultFontSize) else: return 12.0 # Default size @OperationsMethod def SetFontSize(self, ws, size): """ Set the default font size for a writing system. Args: ws: IWritingSystemDefinition object or language tag (str) size (float or int): Font size in points (e.g., 12, 14.5) Raises: FP_ReadOnlyError: If project is not opened with write enabled FP_NullParameterError: If ws or size is None FP_WritingSystemError: If writing system not found FP_ParameterError: If size is not positive Example: >>> ws = list(project.WritingSystems.GetVernacular())[0] >>> project.WritingSystems.SetFontSize(ws, 14) >>> # Set by language tag >>> project.WritingSystems.SetFontSize("en", 12) >>> # Larger size for better readability >>> project.WritingSystems.SetFontSize("qaa-x-kal", 16) Notes: - Size should typically be between 8 and 72 points - Common sizes: 10, 12, 14, 16 - Changes affect all text displayed in this writing system See Also: GetFontSize, SetFontName """ self._EnsureWriteEnabled() self._ValidateParam(size, "size") if size <= 0: raise FP_ParameterError("Font size must be positive") ws_obj = self._ResolveWS(ws) if not ws_obj: raise FP_WritingSystemError(str(ws)) # Set default font size. hasattr guard outside the bracket -- a WS # without the property is a true no-op. if hasattr(ws_obj, "DefaultFontSize"): with self._TransactionCM(f"Set writing system font size to {size}"): ws_obj.DefaultFontSize = float(size) @OperationsMethod def GetRightToLeft(self, ws): """ Get the right-to-left directionality setting for a writing system. Args: ws: IWritingSystemDefinition object or language tag (str) Returns: bool: True if right-to-left, False if left-to-right Raises: FP_NullParameterError: If ws is None FP_WritingSystemError: If writing system not found Example: >>> ws = list(project.WritingSystems.GetVernacular())[0] >>> rtl = project.WritingSystems.GetRightToLeft(ws) >>> print(f"RTL: {rtl}") RTL: False >>> # Check Arabic >>> if project.WritingSystems.Exists("ar"): ... rtl = project.WritingSystems.GetRightToLeft("ar") ... print(f"Arabic is RTL: {rtl}") Arabic is RTL: True Notes: - RTL languages include Arabic, Hebrew, Persian, Urdu - Most languages are left-to-right (LTR) See Also: SetRightToLeft """ ws_obj = self._ResolveWS(ws) if not ws_obj: raise FP_WritingSystemError(str(ws)) # Get RTL setting if hasattr(ws_obj, "RightToLeftScript"): return bool(ws_obj.RightToLeftScript) elif hasattr(ws_obj, "RightToLeft"): return bool(ws_obj.RightToLeft) else: return False # Default to LTR @OperationsMethod def SetRightToLeft(self, ws, is_rtl): """ Set the right-to-left directionality for a writing system. Args: ws: IWritingSystemDefinition object or language tag (str) is_rtl (bool): True for right-to-left, False for left-to-right Raises: FP_ReadOnlyError: If project is not opened with write enabled FP_NullParameterError: If ws or is_rtl is None FP_WritingSystemError: If writing system not found Example: >>> # Set Arabic to RTL >>> if project.WritingSystems.Exists("ar"): ... project.WritingSystems.SetRightToLeft("ar", True) >>> # Set English to LTR >>> project.WritingSystems.SetRightToLeft("en", False) Notes: - RTL languages: Arabic, Hebrew, Persian, Urdu, etc. - This affects text rendering direction in the UI - Most languages should be LTR (False) See Also: GetRightToLeft """ self._EnsureWriteEnabled() self._ValidateParam(is_rtl, "is_rtl") ws_obj = self._ResolveWS(ws) if not ws_obj: raise FP_WritingSystemError(str(ws)) # Set RTL setting. The hasattr dispatch stays OUTSIDE the brackets so # the "neither property exists" fall-through remains a true no-op. if hasattr(ws_obj, "RightToLeftScript"): with self._TransactionCM("Set writing system right-to-left flag"): ws_obj.RightToLeftScript = bool(is_rtl) elif hasattr(ws_obj, "RightToLeft"): with self._TransactionCM("Set writing system right-to-left flag"): ws_obj.RightToLeft = bool(is_rtl) # --- Default Settings --- @OperationsMethod def SetDefaultVernacular(self, ws): """ Set the default vernacular writing system. Args: ws: IWritingSystemDefinition object or language tag (str) Raises: FP_ReadOnlyError: If project is not opened with write enabled FP_NullParameterError: If ws is None FP_WritingSystemError: If writing system not found FP_ParameterError: If ws is not a vernacular writing system Example: >>> # Set by object >>> ws = list(project.WritingSystems.GetVernacular())[0] >>> project.WritingSystems.SetDefaultVernacular(ws) >>> # Set by language tag >>> project.WritingSystems.SetDefaultVernacular("qaa-x-kal") Notes: - The writing system must be in the vernacular list - This is the primary writing system for lexical entries - Used as fallback when no specific WS is specified See Also: GetDefaultVernacular, SetDefaultAnalysis """ self._EnsureWriteEnabled() ws_obj = self._ResolveWS(ws) if not ws_obj: raise FP_WritingSystemError(str(ws)) # Verify it's a vernacular WS vern_tags = self._GetAllVernacularWSTags() if ws_obj.Id not in vern_tags: raise FP_ParameterError(f"Writing system '{ws_obj.Id}' is not a vernacular writing system") # Set as default self.project.lp.DefaultVernacularWritingSystem = ws_obj @OperationsMethod def SetDefaultAnalysis(self, ws): """ Set the default analysis writing system. Args: ws: IWritingSystemDefinition object or language tag (str) Raises: FP_ReadOnlyError: If project is not opened with write enabled FP_NullParameterError: If ws is None FP_WritingSystemError: If writing system not found FP_ParameterError: If ws is not an analysis writing system Example: >>> # Set by object >>> ws = list(project.WritingSystems.GetAnalysis())[0] >>> project.WritingSystems.SetDefaultAnalysis(ws) >>> # Set by language tag >>> project.WritingSystems.SetDefaultAnalysis("en") Notes: - The writing system must be in the analysis list - This is the primary writing system for glosses and definitions - Used as fallback when no specific WS is specified See Also: GetDefaultAnalysis, SetDefaultVernacular """ self._EnsureWriteEnabled() ws_obj = self._ResolveWS(ws) if not ws_obj: raise FP_WritingSystemError(str(ws)) # Verify it's an analysis WS anal_tags = self._GetAllAnalysisWSTags() if ws_obj.Id not in anal_tags: raise FP_ParameterError(f"Writing system '{ws_obj.Id}' is not an analysis writing system") # Set as default self.project.lp.DefaultAnalysisWritingSystem = ws_obj @OperationsMethod def GetDefaultVernacular(self): """ Get the default vernacular writing system. Returns: IWritingSystemDefinition: The default vernacular writing system Example: >>> ws = project.WritingSystems.GetDefaultVernacular() >>> name = project.WritingSystems.GetDisplayName(ws) >>> tag = project.WritingSystems.GetLanguageTag(ws) >>> print(f"Default vernacular: {name} ({tag})") Default vernacular: Kalaba (qaa-x-kal) Notes: - This is the primary vernacular writing system - Used as default for lexical entries - Every project must have a default vernacular WS See Also: SetDefaultVernacular, GetDefaultAnalysis """ return self.project.lp.DefaultVernacularWritingSystem @OperationsMethod def GetDefaultAnalysis(self): """ Get the default analysis writing system. Returns: IWritingSystemDefinition: The default analysis writing system Example: >>> ws = project.WritingSystems.GetDefaultAnalysis() >>> name = project.WritingSystems.GetDisplayName(ws) >>> tag = project.WritingSystems.GetLanguageTag(ws) >>> print(f"Default analysis: {name} ({tag})") Default analysis: English (en) Notes: - This is the primary analysis writing system - Used as default for glosses and definitions - Every project must have a default analysis WS See Also: SetDefaultAnalysis, GetDefaultVernacular """ return self.project.lp.DefaultAnalysisWritingSystem # --- Utility Methods --- @OperationsMethod def GetDisplayName(self, ws): """ Get the display name (UI label) for a writing system. Args: ws: IWritingSystemDefinition object or language tag (str) Returns: str: Display name (e.g., "English", "Kalaba") Raises: FP_NullParameterError: If ws is None FP_WritingSystemError: If writing system not found Example: >>> ws = list(project.WritingSystems.GetAll())[0] >>> name = project.WritingSystems.GetDisplayName(ws) >>> print(name) English >>> # By language tag >>> name = project.WritingSystems.GetDisplayName("en") >>> print(name) English See Also: GetLanguageTag, GetAll """ ws_obj = self._ResolveWS(ws) if not ws_obj: raise FP_WritingSystemError(str(ws)) return ws_obj.DisplayLabel or ws_obj.Id @OperationsMethod def GetLanguageTag(self, ws): """ Get the language tag (BCP 47 identifier) for a writing system. Args: ws: IWritingSystemDefinition object Returns: str: Language tag (e.g., "en", "fr", "qaa-x-kal") Raises: FP_NullParameterError: If ws is None Example: >>> ws = list(project.WritingSystems.GetVernacular())[0] >>> tag = project.WritingSystems.GetLanguageTag(ws) >>> print(tag) qaa-x-kal Notes: - Returns BCP 47 language tag - Format: language[-script][-region][-variant][-extension] - Examples: "en", "en-US", "qaa-x-kal", "zh-Hans" See Also: GetDisplayName, Exists """ self._ValidateParam(ws, "ws") return ws.Id @OperationsMethod def Exists(self, language_tag): """ Check if a writing system with the given language tag exists and is active. Args: language_tag (str): Language tag to check (e.g., "en", "qaa-x-kal") Returns: bool: True if writing system exists and is active (vernacular or analysis), False otherwise Raises: FP_NullParameterError: If language_tag is None Example: >>> if project.WritingSystems.Exists("en"): ... print("English writing system is active") English writing system is active >>> if not project.WritingSystems.Exists("ar"): ... print("Arabic writing system not found") Arabic writing system not found Notes: - Only checks active (vernacular or analysis) writing systems -- i.e. the same set ``GetAll()`` yields. A tag that is present in the project's LDML store but not currently active (vernacular or analysis) returns False here. Use ``ExistsInStore()`` for the whole-store question, or ``Ensure()`` to both check and activate in one call (issue #250 Defect 1). - Comparison is case-insensitive - Handles both '-' and '_' in tags See Also: GetAll, Create, Delete, ExistsInStore, Ensure """ self._ValidateParam(language_tag, "language_tag") ws = self._GetWSByTag(language_tag) if ws is None: return False active_tags = self._GetAllVernacularWSTags() | self._GetAllAnalysisWSTags() return ws.Id in active_tags @OperationsMethod def ExistsInStore(self, language_tag): """ Check if a writing system with the given language tag exists ANYWHERE in the project's LDML store, whether or not it is currently active. This is the whole-store counterpart to ``Exists()`` (issue #250 Defect 1): a project can hold an inactive LDML for a writing system that was configured and later deactivated, or inherited from a template. That writing system is "present" in the sense this method checks, but it is not usable for reading or writing lexical data until it is active -- see ``Ensure()``. Args: language_tag (str): Language tag to check (e.g., "en", "qaa-x-kal") Returns: bool: True if a writing system with this tag exists anywhere in the store (active or not), False otherwise Raises: FP_NullParameterError: If language_tag is None Example: >>> # A writing system deactivated via the FLEx UI still has an >>> # LDML file on disk -- ExistsInStore() sees it, Exists() does not. >>> project.WritingSystems.Exists("fr") False >>> project.WritingSystems.ExistsInStore("fr") True Notes: - Comparison is case-insensitive and folds '-'/'_' (same normalization as ``Exists()``) - This is the only place in the public API that intentionally answers the whole-store question under that normalization -- do not use it as a stand-in for "is this writing system usable" See Also: Exists, Ensure, Create """ self._ValidateParam(language_tag, "language_tag") return self._GetWSByTag(language_tag) is not None @OperationsMethod def GetBestString(self, string_obj): """ Extract the best analysis or vernacular string from a MultiString or MultiUnicode object. This method intelligently selects the most appropriate string alternative from a multi-writing-system text object, preferring analysis writing systems first, then falling back to vernacular writing systems. Args: string_obj: IMultiUnicode or IMultiString object containing text in multiple writing systems Returns: str: The best available string, or empty string if no valid text found Raises: FP_NullParameterError: If string_obj is None FP_ParameterError: If string_obj is not IMultiUnicode or IMultiString Example: >>> # Get the best string from a lexeme form >>> entry = project.LexEntry.Find("test") >>> best_text = project.WritingSystems.GetBestString(entry.LexemeFormOA.Form) >>> print(best_text) test >>> # Get best gloss from a sense >>> sense = entry.SensesOS[0] >>> best_gloss = project.WritingSystems.GetBestString(sense.Gloss) >>> print(best_gloss) a sample word Notes: - Prefers analysis writing systems over vernacular - Returns empty string for "***" (FLEx's null string marker) - Useful for extracting displayable text from multi-WS fields - This is a convenience method for common text extraction patterns See Also: GetAll, GetAnalysis, GetVernacular """ self._ValidateParam(string_obj, "string_obj") # Import types locally to avoid circular imports from SIL.LCModel.Core.KernelInterfaces import IMultiUnicode, IMultiString if not isinstance(string_obj, (IMultiUnicode, IMultiString)): raise FP_ParameterError("GetBestString: string_obj must be IMultiUnicode or IMultiString") # Get the best alternative (analysis preferred, then vernacular) s = string_obj.BestAnalysisVernacularAlternative.Text # Return empty string for FLEx's null marker "***" return "" if s == "***" else s # --- Private Helper Methods --- def _GetAllVernacularWSTags(self): """ Get set of all vernacular writing system tags. Returns: set: Set of vernacular WS language tags """ return set(self.project.lp.CurVernWss.split()) def _GetAllAnalysisWSTags(self): """ Get set of all analysis writing system tags. Returns: set: Set of analysis WS language tags """ return set(self.project.lp.CurAnalysisWss.split()) def _NormalizeLangTag(self, language_tag): """ Normalize language tag for comparison (lowercase, underscores to hyphens). Args: language_tag (str): Language tag to normalize Returns: str: Normalized tag """ return language_tag.replace("_", "-").lower() def _GetWSByTag(self, language_tag): """ Get writing system object by language tag. Args: language_tag (str): Language tag Returns: IWritingSystemDefinition or None: Writing system or None if not found """ normalized_tag = self._NormalizeLangTag(language_tag) for ws in self.project.project.ServiceLocator.WritingSystems.AllWritingSystems: if self._NormalizeLangTag(ws.Id) == normalized_tag: return ws return None def _GetWSByHandle(self, handle): """ Get writing system object by handle. Args: handle (int): Writing system handle Returns: IWritingSystemDefinition or None: Writing system or None if not found """ for ws in self.project.project.ServiceLocator.WritingSystems.AllWritingSystems: if ws.Handle == handle: return ws return None def _ResolveWS(self, ws): """ Resolve a writing system parameter to a WS object. Args: ws: Either IWritingSystemDefinition object or language tag (str) Returns: IWritingSystemDefinition or None: Writing system object or None """ self._ValidateParam(ws, "ws") if isinstance(ws, str): return self._GetWSByTag(ws) else: # Already a writing system object return ws @OperationsMethod def Duplicate(self, item_or_hvo, insert_after=True, deep=False): """ Duplicate operation is not applicable for writing systems. Writing systems are project-level configuration objects with unique identifiers and cannot be duplicated in the traditional sense. Args: insert_after (bool): Accepted for API uniformity across Operations classes. Ignored -- this method always raises. deep (bool): Accepted for API uniformity across Operations classes. Ignored -- this method always raises. Raises: NotImplementedError: Always raised - writing systems cannot be duplicated. Notes: - Use Create() to create a new writing system with similar properties - Writing systems have unique language tags (BCP 47) - Each writing system must have a unique identifier - Duplication would create ambiguity in the WS system See Also: Create, Delete, GetAll """ raise NotImplementedError("Writing systems cannot be duplicated. Use Create() to create a new writing system.") # ========== SYNC INTEGRATION METHODS ========== @OperationsMethod def GetSyncableProperties(self, item): """ Get syncable properties - NOT IMPLEMENTED for writing systems. Writing systems are linguistic configuration unique to each project and should not be synced between projects. Raises: NotImplementedError: Writing systems are not syncable """ raise NotImplementedError( "Writing systems cannot be synced between projects. " "Writing systems are linguistic configuration unique to each project." ) @OperationsMethod def CompareTo(self, item1, item2, ops1=None, ops2=None): """ Compare writing systems - NOT IMPLEMENTED. Writing systems are linguistic configuration unique to each project and should not be synced between projects. Raises: NotImplementedError: Writing systems are not syncable """ raise NotImplementedError( "Writing systems cannot be compared for sync. " "Writing systems are linguistic configuration unique to each project." )