diff --git a/docs/ar/csharp-differences.md b/docs/ar/csharp-differences.md new file mode 100644 index 0000000..43009ee --- /dev/null +++ b/docs/ar/csharp-differences.md @@ -0,0 +1,52 @@ +# الاختلافات عن مكتبات C\# + +تُعتبر «sil-lift» مشابهة بشكل عام لأدوات LIFT الخاصة بـ SIL المكتوبة بلغة C# — وبشكل أساسي `SIL.Lift` الموجودة في [libpalaso](https://github.com/sillsdev/libpalaso) (المحلل النحوي، أداة التحقق من الصحة، أداة الترحيل، `LiftSorter`)، و`SIL.DictionaryServices` في نفس المستودع (نموذج `LexEntry`/`LexSense`، مع قارئ/كاتب LIFT خاص به، والذي يستخدمه كل من The Combine و WeSay)، ومعالجات LIFT في [Chorus](https://github.com/sillsdev/chorus). إنه تطبيق جديد، وليس نسخة معدلة. تلخص هذه الصفحة النقاط التي يختلف فيها السلوك بشكل متعمد. + +## النطاق + +| القدرة | مكتبات C# | سيل-ليفت | +| ----------------- | -------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------- | +| إصدارات LIFT | 0.10–0.13 (مع ميزة الترحيل المدمجة) | **0.13 فقط**؛ يتم رفض الإصدارات الأقدم مع ظهور خطأ واضح | +| ترحيل الإصدار | `Migrator` (سلسلة XSLT) | لا شيء — استخدم ملفات XSLT الموجودة في «lift-standard» لإجراء عمليات الترقية لمرة واحدة | +| دمج/مزامنة ثلاثية | الكورس | خارج النطاق | +| التحقق من الصحة | RELAX NG فقط (`Validator`) | RELAX NG + نطاقات المخطط + الفحوصات الدلالية | +| البث المباشر | التحليل الداخلي على مستوى العناصر الفردية | واجهة برمجة التطبيقات العامة `open_reader` / `open_writer` | + +## شكل واجهة برمجة التطبيقات (API) + +يعمل محلل `SIL.Lift` على أساس آلية الاستدعاء المرتد (`ILexiconMerger`): فهو يقوم بدفع أحداث التحليل إلى المستلم. بدلاً من ذلك، تُرجع «sil-lift» مخطط كائنات عادي — فئات بيانات مُحددة النوع لكل عنصر من عناصر LIFT — لأن مبرمجي بايثون يفضلون الكائنات، وليس دالات الاستدعاء. تقوم `SIL.DictionaryServices` بتركيب نموذج كائنات `LexEntry`/`LexSense` فوق `SIL.Lift`، ولكنه كنموذج تطبيق لا يمثل سوى البنيات التي تستخدمها تلك التطبيقات — لذا فإن إعادة التسلسل من خلاله لا يمكنها الحفاظ على المحتوى خارج النموذج بالطريقة التي تتبعها معالجة بقايا LIFT ودقة البايت في sil-lift (انظر أدناه). تُنتج واجهة برمجة التطبيقات (API) الخاصة بالتدفق النوع _نفسه_ من `Entry`، لذا لا يوجد نموذج ثانٍ مبسط يتعين تعلمه. + +## دقة النقل في كلا الاتجاهين + +أقوى اختلاف متعمد. يؤدي الحفظ باستخدام `SIL.Lift` إلى إعادة تسلسل المستند بأكمله. تضمن شركة «سيل-ليفت» ما يلي: + +- يتم حفظ المستند الذي لم يطرأ عليه تغيير **بشكل مطابق تمامًا من حيث البايتات**، و +- تحتفظ الإدخالات التي لم يتم التعديل عليها ببايتات المصدر الدقيقة الخاصة بها حتى عند تغيير الإدخالات الأخرى — وهو نفس أسلوب تقسيم البايتات لكل إدخال الذي يستخدمه «Chorus»، ويتم تطبيقه تلقائيًا. + +انظر [ضمانات فيديليتي](fidelity.md). + +## التحقق من الصحة + +يقوم `Validator` في لغة C# بتنفيذ دورة واحدة من RELAX NG ويُبلغ عن الأخطاء الأولى في شكل سلاسل نصية. يُبلغ sil-lift عن دفق منظم من «المشكلات» (Problem)، يحمل كل منها الملف والمدخل والسطر الذي يتعلق به، وتختلف طبقة مخططه عن قصد في ثلاثة مواضع: + +- **عناوين URI غير الصالحة تُعتبر تحذيرات وليست أخطاء.** لم يفرض محرك C# RELAX NG أبدًا نوع البيانات `anyURI`، لذا ظل برنامج FieldWorks (FLEx) لسنوات عديدة يدرج روابط href من النوع `file://C:/...` في المعاجم الفعلية. إن رفض تلك الملفات سيؤدي إلى وضع علامة على كل ملفات التصدير من FLEx تقريبًا. +- **يتم تطبيق قواعد Schematron** (كعمليات تدقيق دلالية): تم تجاهل لغات النماذج المكررة والقيود المشتركة المماثلة في قواعد LIFT دون إشعار من قبل كل من عملية التحقق من صحة C# وعملية التحقق من صحة lxml الخام. +- **يتم توحيد المقارنات بين الملفات وفقًا لمعايير يونيكود**، لأن برنامج FLEx يكتب ملف `.lift` بتنسيق NFC، بينما يكتب الملف المصاحب `.lift-ranges` بتنسيق NFD. + +يقوم sil-lift أيضًا بالتحقق من صحة الملفات المصاحبة `.lift-ranges` الخاصة بالمعجم الذي تم تحميله، مقارنةً بمخطط خاص بوثائق النطاقات المستقلة (المقدمة من `lift-standard` جنبًا إلى جنب مع قواعد LIFT الأساسية) — يتم فحص كل ملف نطاقات خارجي يتم تتبعه عند كل عملية التحقق من صحة ملف `.lift` — في حين لا يوجد مثل هذا المخطط (أو الفحص) في بيئة C#. (لا توجد نقطة دخول للتحقق من صحة ملف `.lift-ranges` بمفرده، بمعزل عن ملف `.lift`.) + +## الترتيب القياسي + +تعكس دالة `Lexicon.sort()` القواعد الأساسية لـ `LiftSorter` (المدخلات حسب معرّف فريد (GUID) دون التمييز بين الأحرف الكبيرة والصغيرة؛ النطاقات وعناصر النطاقات حسب المعرّف (ID)؛ تعريفات حقول الرؤوس حسب العلامة (tag)؛ يتم الحفاظ على المعاني حسب ترتيبها في الملف؛ ولا يتم المساس أبدًا بالمسافات البيضاء داخل ``)، مع وجود ثلاثة اختلافات: + +- يتم فرز الإدخالات التي لا تحتوي على معرّف فريد (GUID) بشكل حتمي حسب المعرّف (ID) (يفترض `LiftSorter` وجود معرّف فريد (GUID))؛ +- الترتيب مستقل عن الإعدادات المحلية (نقاط الرموز بعد إزالة التمييز بين الأحرف الكبيرة والصغيرة، وليس ترتيب .NET المستقل عن الثقافة)؛ +- تحتفظ القوائم من نفس النوع — مثل الملاحظات والعلاقات والنماذج — بترتيبها في المستند بدلاً من إعادة فرزها حسب المفتاح — فالترتيب في مجموعات أمر حتمي بالفعل، وإعادة ترتيبها لا يؤدي إلا إلى زيادة التشويش في ملفات المقارنة. + +لا يتم استخدام ملف `canonicalizeLift.xsl` الموجود في مستودع المواصفات على الإطلاق: فهو يدمج المسافات البيضاء داخل النص اللغوي (عملية تدميرية)، كما أن المعرّفات التي يولدها تختلف في كل عملية تشغيل. + +## لم يتم ترحيله + +- الميزات الخاصة بـ WeSay (لوحة التحكم/إدارة الإعدادات المتعلقة بملفات LIFT). +- `SynchronicMerger` (دمج تحديثات Chorus) — فكرة تقسيم البيانات إلى مقاطع بايت لا تزال قائمة في طبقة الدقة، أما عملية الدمج فلم تعد موجودة. +- تحليل نظام الكتابة LDML: تُعامل الملفات الموجودة في `WritingSystems/` على أنها محتوى مجلد غير شفاف. diff --git a/docs/ar/fidelity.md b/docs/ar/fidelity.md new file mode 100644 index 0000000..ca15f5a --- /dev/null +++ b/docs/ar/fidelity.md @@ -0,0 +1,33 @@ +# ضمانات فيديليتي + +LIFT هو تنسيق _تبادل_، لذا فإن القاعدة الأولى هي **ألا تتجاهل أبدًا ما لا تفهمه**. عقد `sil-lift`، الذي يتم التحقق من صحته بواسطة مجموعة الاختبارات في كل عملية تشغيل (ملفات المجموعة بالإضافة إلى التوليد القائم على الخصائص): + +## القراءة + +يتم تحميل أي مستند LIFT 0.13 صحيح التكوين — بما في ذلك المحتوى غير المتوافق مع المخطط. كل ما لا يحدده النموذج يتم نقله في حاوية `Extras` غير الشفافة الخاصة بالعقدة الأقرب باعتباره _بقايا LIFT_ — وهو الاسم الذي تستخدمه FieldWorks للإشارة إلى نفس المفهوم، والذي يتم تخزينه في حقل `LiftResidue`: السمات والعناصر غير المعروفة، وتعليقات XML وتعليمات المعالجة، والنصوص المتناثرة، والسمات المكتوبة ذات التنسيق الخاطئ (يُحتفظ بالتاريخ غير الصحيح كسلسلة نصية أصلية في `Extras`؛ ويكون الحقل المكتوب `None`). + +## حفظ مستند لم يتم تغييره + +تؤدي وظيفة `load()` → `save()` دون إجراء أي تعديلات إلى كتابة **مخرجات متطابقة على مستوى البايت** — دون إعادة تنسيق، أو إعادة ترميز، أو إعادة ترتيب، مع تضمين علامات ترتيب البايت وإعلانات XML. لا توجد حاليًا قائمة تطبيع: الهوية دقيقة. + +الاستثناءات (يلجأ الكاتب إلى التسلسل القياسي الكامل، الذي يعتبر كاملاً من الناحية الدلالية ولكنه لا يحافظ على البايتات): + +- ترميز المصدر غير متوافق مع ASCII (ليس UTF-8/US-ASCII)، أو +- يحتوي الملف المصدر على تعريف نوع المستند (DOCTYPE)، أو +- يختلف كل من الماسح البايت والمحلل حول البنية العليا للوثيقة — على سبيل المثال، وجود `
` ثانية خارج المواصفات، والتي يحتفظ بها المحلل مرة واحدة فقط (أما الماسح فهو متحفظ عن عمد: ففي حالة وجود أي شك، لا يلتقط أي بايتات من المصدر على الإطلاق)، أو +- تم إنشاء المصدر في الذاكرة بدلاً من تحميله من ملف. + +## حفظ مستند تم تحريره + +- **يتم إخراج الإدخالات التي لم يتم التعديل عليها حرفياً من البايتات الأصلية.** يُعتبر الإدخال قد تم التعديل عليه إذا تغير أي جزء من كائن النموذج الخاص به منذ عملية التحليل (يتم الكشف عن ذلك من خلال لقطة التسلسل القياسي، وليس من خلال علامة التغيير).\*\* +- **يتم إعادة تسلسل المدخلات التي تم تعديلها بشكل قياسي وكامل**: UTF-8، مسافة بادئة بمقدار مسافتين _خارج_ المحتوى المختلط (لا يتم تغيير المسافات البيضاء داخل `` و`` أبدًا)، وتجميع فرعي موثق لكل عنصر (على سبيل المثال، المدخل: الوحدة اللغوية، الاقتباس، طرق النطق، المتغيرات، المعاني، الملاحظات، العلاقات، أصول الكلمات، التعليقات التوضيحية، السمات، الحقول)، ترتيب ثابت للسمات، التواريخ وفقًا لمعيار ISO-8601 (حرف `Z` لـ UTC). يتم إعادة إصدار جميع البقايا؛ ويتم إعادة وضعها في الفهرس الفرعي الأصلي، مع ربطها بالقائمة الفرعية الجديدة (وهذا تقريب — فالمواضع الدقيقة بالبايت مضمونة فقط للمدخلات التي لم يتم التعديل فيها). +- تؤدي إضافة العناصر أو حذفها أو إعادة ترتيبها إلى إعادة تسلسل بنية المستند، لكنها تظل تُخرج بايتات كل عنصر لم يتغير حرفياً. + +!!! note ""ملف XML القياسي" المذكور هنا لا علاقة له بأي ملف XML قياسي آخر" + يُقصد بالصيغة القياسية في هذه الصفحة الشكل الموثق الخاص بـ «sil-lift»، والموصوف في إحدى النقاط أعلاه. ولا علاقة له بعملية «Canonical XML (C14N)» التابعة لـ W3C. ولا علاقة له بفئة `CanonicalXmlSettings` التابعة لـ `SIL.Core`. + +## التقريبات المعروفة (العقد التي تم لمسها فقط) + +- يتم الاحتفاظ بالتعليقات الموجودة _داخل_ عملية تشغيل ``، لكن يتم نقلها إلى جانب عملية التشغيل، ولا يتم الاحتفاظ بها في موضعها الدقيق من حيث عدد الأحرف. +- يتم توحيد ترتيب العناصر الفرعية من النوع «المتقاطع» داخل عنصر تم تعديله وفقًا للتجميع القياسي (تجعل خاصية `interleave` في مخطط LIFT هذا الترتيب غير ذي أهمية من الناحية الدلالية). +- لا يتم إعادة إصدار عنصر متعدد النصوص موجود ولكنه لا يحمل أي شيء — لا نماذج ولا بقايا، على سبيل المثال `` —. يمثل النموذج هذه الحقول على شكل `Multitext` موجود دائمًا (`lexical-unit`، و`citation`، و`definition`، و`usage` للعلاقة، و`label` / `abbrev` / `description` في مراجع عناوين الويب، والنطاقات، وعناصر النطاق، والعنوان)، لذا لا يمكن التمييز بين النموذج الفارغ والنموذج الغائب بعد التحليل. لا يُفقد أي شيء من الناحية الدلالية. diff --git a/docs/ar/guides/build-export.md b/docs/ar/guides/build-export.md new file mode 100644 index 0000000..b4d269e --- /dev/null +++ b/docs/ar/guides/build-export.md @@ -0,0 +1,168 @@ +# مثال عملي: إنشاء عملية تصدير LIFT من البداية + +إذا كنت تقوم بتصدير بيانات تطبيق آخر بتنسيق LIFT — وهي المهمة التي تقف وراء [إنشاء ملفات LIFT متوافقة](lift-export-interop.md) — فيمكن لـ `sil-lift` إنشاء المستند كائنًا تلو الآخر وتسلسله، بدلاً من إنشاء ملف XML يدويًّا. يشرح هذا المثال أحد البرامج النصية التي تقوم بإنشاء مدخل يتضمن العناصر التي يتألف منها القاموس الفعلي (أنظمة كتابة متعددة، ونطق، ومعنى مع مثال، ورسم توضيحي، وسمة المجال الدلالي، وحقل خاص بالتطبيق)، وتقوم بكتابة المفردات المحددة في ملف مصاحب بتنسيق `.lift-ranges`، ثم تتحقق من صحتها، وتحفظها. + +## النص + +```python +from pathlib import Path + +import sil_lift + +lex = sil_lift.Lexicon(producer="my-exporter") + +# إدخال واحد، تم إنشاؤه من النموذج المصدر. +entry = sil_lift.Entry(id="kanga", guid="6b9e7c2a-3f4d-4a1b-8c5e-2d9f0a1b2c3d") +entry.lexical_unit["seh"] = "nkhuku" +entry.lexical_unit["pt"] = "galinha" + +pron = sil_lift.Pronunciation() +pron.forms["en"] = "المتحدث: آنا" # اتفاقية تسمية المتحدث في "The Combine" +pron.media.append(sil_lift.URLRef(href="audio/nkhuku.wav")) +entry.pronunciations.append(pron) + +sense = sil_lift.Sense(id="kanga_s1") +sense.grammatical_info = sil_lift.GrammaticalInfo(value="Noun") +sense.glosses.append(sil_lift.Form(lang="en", text=sil_lift.Text(["chicken"]))) +sense.definition["en"] = "a domestic fowl kept for its eggs and meat" + +example = sil_lift.Example() +example.forms["seh"] = "Ndinafuna nkhuku." +translation = sil_lift.Translation() +translation.forms["en"] = "I want a chicken." +example.translations.append(translation) +sense.examples.append(example) + +photo = sil_lift.URLRef(href="pictures/hen.jpg") +photo.label["en"] = "A hen" +sense.illustrations.append(photo) + +sense.traits.append(sil_lift.Trait(name="semantic-domain-ddp4", value="1.6.1.2")) + +scientific = sil_lift.Field(type="scientific-name") # حقل إضافي خاص بالتطبيق +scientific.content["en"] = "Gallus gallus domesticus" +sense.fields.append(scientific) + +entry.senses.append(sense) +lex.entries.append(entry) + +# المفردات المراقبة التي يشير إليها الإدخال، في ملف .lift-ranges مصاحب. +ranges = sil_lift.RangesFile() +ranges.add_range("grammatical-info").add_element("Noun").label["en"] = "noun" +ranges.add_range("semantic-domain-ddp4").add_element("1.6.1.2").label["en"] = "Bird" +lex.add_ranges_file(ranges, href="birds.lift-ranges") + +# التحقق من صحة ما ستكتبه save()، قبل الكتابة على القرص. +problems = list(lex.iter_problems()) +print(f"التحقق من الصحة: {len(problems)} مشكلة (مشاكل)") + +out = Path("export") +out.mkdir(exist_ok=True) +lex.save(out / "birds.lift") +print("=== birds.lift ===") +print((out / "birds.lift").read_text(encoding="utf-8"), end="") +print("=== birds.lift-ranges ===") +print((out / "birds.lift-ranges").read_text(encoding="utf-8"), end="") +``` + +## ما الذي تنتجه + +`التحقق من الصحة: 0 مشكلة(مشاكل)`، ثم `.lift` وما يرافقه جنبًا إلى جنب: + +``` +=== birds.lift === + + +
+ + + + +
+ + +
+ nkhuku +
+
+ galinha +
+
+ +
+ Speaker: Ana +
+ +
+ + + + chicken + + +
+ a domestic fowl kept for its eggs and meat +
+
+ +
+ Ndinafuna nkhuku. +
+ +
+ I want a chicken. +
+
+
+ + + + + +
+ Gallus gallus domesticus +
+
+
+
+
+=== birds.lift-ranges === + + + + + + + + + + + + + +``` + +## ملاحظات حول واجهة برمجة التطبيقات (API) + +- الحقول متعددة النصوص (`lexical_unit`، `definition`، تسمية `Form`/`URLRef`، محتوى `Field`، ...) يتم أخذ سلسلة واحدة لكل نظام كتابة عبر واجهة التعيين: `entry.lexical_unit["seh"] = "nkhuku"` تضيف `
`. ويتم تعيين نموذج المصدر الذي يربط السلاسل برموز اللغات مباشرةً إلى هذا النموذج. +- `RangesFile.add_range()` / `Range.add_element()` تُنشئ المفردات المُحددة، و`Lexicon.add_ranges_file(ranges, href=...)` تُرفق الملف المصاحب وتضيف رأس الملف `` — بحيث يتم تحويل `` و`` الخاصين بالمدخل إلى النطاقات التي حددتها. +- `URLRef` هو رابط href مصحوب بنص متعدد اختياري للتعليق أو التسمية — ويُستخدم لكل من `` (الصوت) و`` (الصور). يتبع النطق هنا قاعدة «ذا كومباين» الخاصة بالصيغة «en» التي تُقرأ على النحو التالي: «المتحدث: ». +- البيانات الخاصة بالتطبيق التي لا تحتوي على رحلات LIFT محلية كـ « » (أو « »): يقرأ تطبيق FieldWorks هذه البيانات كحقول مخصصة، ويحتفظ تطبيق The Combine بها. +- قم بتعيين `guid` حقيقي وثابت لكل إدخال (على سبيل المثال، من `uuid.uuid4()`، مع إعادة استخدامه عبر عمليات التصدير) — حيث تؤدي عملية إعادة الاستيراد لاحقًا إلى تحديث الإدخال في مكانه بدلاً من تكراره. يضمن الأمر `sil-lift validate --require-ids` تطبيق ذلك. +- تقوم الدالة `lex.iter_problems()` بالتحقق من صحة المستند الموجود في الذاكرة (ما ستقوم الدالة `save()` بكتابته) قبل أن يتم تسجيل أي شيء على القرص؛ وهو هنا خالٍ من الأخطاء. نظرًا لعدم وجود مجلد للمعجم حتى الآن، يتم تخطي فحوصات وجود الوسائط ورابط الملف المصاحب — قم بتشغيل [`sil-lift validate`](cli.md) على المخرجات المحفوظة (أو باستخدام الخيار `--no-check-media`) بمجرد توفر ملفات الصوت والصور. + +## التغليف + +يؤدي الأمر `lex.save("export/birds.lift")` إلى كتابة المجلد بالصيغة التالية (ملفان `.lift` و`.lift-ranges` جنبًا إلى جنب). لإنشاء حزمة مضغوطة واحدة يمكن لبرنامجي FieldWorks و The Combine استيرادها مباشرةً، استخدم `lex.save_zip("birds.zip")` بدلاً من ذلك — انظر [إنتاج ملفات LIFT متوافقة](lift-export-interop.md). diff --git a/docs/ar/guides/bulk-edit-glosses.md b/docs/ar/guides/bulk-edit-glosses.md new file mode 100644 index 0000000..7359cef --- /dev/null +++ b/docs/ar/guides/bulk-edit-glosses.md @@ -0,0 +1,79 @@ +# مثال توضيحي: التحرير الجماعي للتعليقات التوضيحية + +مهمة صيانة شائعة: توحيد قواعد الإملاء في جميع المصطلحات الإنجليزية الواردة في المعجم (من البريطانية إلى الأمريكية، أو العكس) دون المساس بأي عنصر آخر في الملف. يشرح هذا المثال أحد البرامج النصية التي تقوم بالتحميل والتحرير والتحقق من الصحة والحفظ — موضحًا كيفية عمل واجهة برمجة التطبيقات الخاصة بالتحرير وضمان الدقة معًا. + +## النص + +```python +import sys + +import sil_lift + +path = "dictionary.lift" +lex = sil_lift.load(path) + + +def iter_senses(senses): + """تسليم كل معنى، بما في ذلك المعاني الفرعية (بشكل متكرر).""" + for sense in senses: + yield sense + yield from iter_senses(sense.subsenses) + + +edited_glosses = 0 + +for entry in lex.entries: + for sense in iter_senses(entry.senses): + for gloss in sense.glosses: + if gloss.lang != "en": + continue + old = str(gloss.text) + new = old.replace("colour", "color") + if new != old: + gloss.text = sil_lift.Text([new]) + edited_glosses += 1 + +changed = lex.changed_entries() + +errors = [p for p in lex.iter_problems() if p.level == "error"] +if errors: + for problem in errors: + print(problem) + sys.exit(f"إلغاء العملية: {len(errors)} خطأ (أخطاء) في التحقق من الصحة، لم يتم حفظ أي شيء") + +lex.save() +print(f"تم تحرير {edited_glosses} التعريفات في {len(changed)} مدخل (مداخل)") +``` + +بعض النقاط الجديرة بالملاحظة: + +- تعد `Sense.subsenses` بحد ذاتها `list[Sense]`، لذا فإن `iter_senses` تتكرر بداخلها — أما عملية التحرير الجماعي التي تقتصر على `entry.senses` فستتجاهل بصمت أي تفسير متداخل تحت معنى فرعي. +- `gloss.text` هو كائن من نوع `Text`، وليس سلسلة نصية عادية: تعمل الدالة `str(gloss.text)` على تحويله إلى سلسلة نصية عادية لأغراض المطابقة، ويتم كتابة النص البديل باستخدام `sil_lift.Text([new])` بدلاً من تعديل السلسلة في مكانها. +- تُظهر الدالة `lex.changed_entries()` المدخلات التي تختلف عن الملف كما تم تحميله. نظرًا لأن ملخص المدخل يغطي شجرته الفرعية بأكملها، فإن أي تعديل يُجرى على معنى فرعي متداخل يُبلغ المدخل الذي يحتوي عليه. + - إنها تقارن المحتوى المُسلسل، لذا فإن تعيين قيمة كان الحقل يحملها بالفعل لا يتم الإبلاغ عنه. + - وهي تُبلغ عن التغييرات في المحتوى فقط؛ أما `lex.added_entries()` و`lex.removed_entries()` فتشملان المدخلات التي ظهرت أو اختفت منذ بدء التحميل. + - تُرجع هذه الدالة السجلات نفسها، دون أن يتأثر ذلك بتكرار قيمة `id` أو عدم وجودها (وهو ما يسمح به LIFT). + - كإحصاء، لا يكون لهذا معنى إلا إذا كان هناك ما يُقارن به. عندما يرفض الماسح البايت قراءة المصدر — سواء بسبب ترميز غير متوافق مع ASCII، أو بسبب خلاف بين الماسح والمحلل — لا يكون هناك خط أساس، وتقوم الدالة `changed_entries()` بالإبلاغ عن _كل_ إدخال. هذه هي الإجابة الصادقة فيما يتعلق بـ«حارس الكتابة»، حيث إن دالة `save()` تعيد تسلسل الملف بأكمله في تلك الحالة، لكن هذا يعني أن العدد يمثل حجم المعجم بدلاً من حجم التعديل. +- تُبيّن الدالة `lex.changes()` ما إذا كان المستند قد تغير _على الإطلاق_. وهو لا يشمل العناصر فحسب، بل يشمل أيضًا العنصر الرئيسي والعنصر الجذري وكل عنصر مصاحب من نوع `.lift-ranges`. + - ولا يكون ذلك خطأً إلا عندما تعيد دالة `save()` إنتاج البايتات الأصلية، مما يجعل `if not lex.changes(): ...` الطريقة الصحيحة لتخطي عملية كتابة غير ضرورية. هذا الضمان يعمل في اتجاه واحد: فهو لا يُبلغ أبدًا بوجود «لا شيء لتدوينه» بالنسبة لمستند من المقرر إعادة كتابته، في حين أن التغيير الذي يفرض إعادة تسلسل كاملة قد يعود إلى البايتات الأصلية ويُبلغ عنه مع ذلك. + - فهي تقارن المحتوى، وليس الوجهة، لذا لا تستخدمها إلا لحفظ الملف في مكانه الأصلي: `lex.save(some_other_dir / "dictionary.lift")` تكتب المستند وملحقاته إلى موقع لا يحتوي على أي شيء بعد، بغض النظر عما إذا كان قد طرأ أي تغيير أم لا. + - إنها عملية حماية، وليست عملية تسريع — فالاستجابة لها تتطلب معالجة كل إدخال، وهو نفس العمل الذي تقوم به وظيفة `save()` لتحديد البايتات المصدرية التي يمكن إعادة استخدامها؛ لذا فإن ما تتخطاه هو عملية الكتابة نفسها (أي بقاء وقت تعديل الملف دون تغيير، وعدم ظهور فروق زائفة)، وليس الجهد المبذول في عملية التحديد. +- تقوم عملية التحقق من الصحة في الذاكرة (`lex.iter_problems()`) بتسلسل الحالة التي تم تعديلها أولاً، بحيث تعكس التعديل بشكل صحيح قبل كتابة أي شيء على القرص. إن إيقاف العملية عند حدوث أي `"خطأ"` من فئة `Problem` — حيث تُترك التحذيرات للمستدعي لاتخاذ القرار بشأنها — يعني أن عملية التحرير الخاطئة لن تصل أبدًا إلى `save()`. + +اللمعان ليس الشيء الوحيد الذي يستحق أن نلمسه بهذه الطريقة. ينطبق سطح التعيين «Multitext» نفسه على التعريفات وعلى كل حقل متعدد اللغات آخر في أي مدخل أو معنى: + +```python +sense.definition["en"] = "لون الشيء" +``` + +## تشغيله + +قم بإجراء مقارنة مع معجم صغير يحتوي على تفسير رئيسي وتفسير فرعي، وكلاهما يشير إلى كلمة «colour»: + +``` +تم تعديل مصطلحين في مدخلة واحدة +``` + +## مكافأة الإخلاص + +يتم تطبيق الضمان على كل _قيمة_ على حدة: فالقيمة التي لم يتغير نموذجها تُعاد **بشكل مطابق تمامًا من حيث البايتات** كما تم قراءتها في البداية، ولا يتم إعادة تسلسل سوى القيم التي قمت بالتعديل عليها فعليًّا. في المقطع أعلاه، تم تعديل التعليقات التوضيحية في أحد المدخلات — بينما احتفظت جميع المدخلات الأخرى في الملف ببايتاتها بالضبط. (لاحظ مستوى التفصيل: يؤدي تعديل أي جزء من المدخل إلى إعادة تسلسل المدخل بأكمله، بما في ذلك المعاني المماثلة التي لم يتم تعديلها.) وبالتالي، فإن تعديل تعريف واحد في معجم يضم 50,000 مدخلاً ينتج عنه ملف «diff» يؤثر على مدخل واحد فقط، وليس ملفًا معاد تنسيقه. انظر [ضمانات فيديليتي](../fidelity.md) للاطلاع على نص العقد الدقيق. diff --git a/docs/ar/guides/cli.md b/docs/ar/guides/cli.md new file mode 100644 index 0000000..877e66a --- /dev/null +++ b/docs/ar/guides/cli.md @@ -0,0 +1,70 @@ +# سطر الأوامر + +يؤدي تثبيت الحزمة (`pip install sil-lift`) أيضًا إلى تثبيت الأمر `sil-lift` — وهي أداة مدعومة تتماشى مع روح LiftTools وتأتي مرفقة مع الحزمة (كما أنها، بالنسبة لـ `validate`، تمثل مثالاً عمليًّا على واجهة برمجة تطبيقات المكتبة). + +``` +sil-lift validate PATH [--format {text,json}] [--strict] [--no-check-media] [--require-ids] + جميع المشكلات، مع الإشارة إلى الملف/المدخل/السطر؛ الخروج برمز 1 في حالة وجود أخطاء +sil-lift stats PATH [--format {text,json}] + عدد المدخلات/المعاني/اللغات (بشكل متواصل؛ أي حجم) +sil-lift sort PATH [-o OUT] نسخة مرتبة بشكل قياسي وجاهزة للمقارنة (الافتراضي: في المكان نفسه) +sil-lift check-media PATH تقرير الوسائط المفقودة واليتيمة؛ الخروج برمز 1 في حالة وجود وسائط مفقودة +sil-lift export PATH [-o OUT] [--langs L] [--tsv] + صف واحد لكل معنى فرعي (تم تسطيح المعاني الفرعية) إلى CSV/TSV (بشكل متواصل) +``` + +تقوم الخيار `--format json` بكتابة كائن JSON واحد إلى stdout (ولا شيء غير ذلك) لاستخدامه في عمليات التكامل المستمر (CI) والأتمتة؛ انظر المخطط في المثال أدناه. يعامل الخيار `--strict` التحذيرات كأخطاء، ويُخرج القيمة 1 في حالة العثور على أي منها — استخدمه لضمان أن عملية البناء لا تتضمن أي تحذيرات على الإطلاق، بدلاً من الاكتفاء بالأخطاء وحدها. يتخطى الخيار `--no-check-media` عملية التحقق من وجود الوسائط في نظام الملفات (مما يؤدي إلى إخفاء نتائج `missing-media`)، وهو أمر مفيد عند التحقق من صحة ملف تصدير تم إنشاؤه حديثًا وتوجد ملفات الصوت/الصور الخاصة به في مكان آخر وليس في نفس المجلد. كما أن `--require-ids` تُسفر عن فشل (خطأ `missing-id`) في حالة أي إدخال يفتقر إلى `guid` أو أي معنى يفتقر إلى `id` — وهو أكثر صرامة من LIFT، وذلك بالنسبة لسير العمل التي تعيد الاستيراد باستخدام معرّف ثابت. عند تمرير `-` كمسار، يتم قراءة المستند من المدخل القياسي (لا يحتوي المستند الممرر عبر الأنابيب على مجلد، لذا لا يتم تحديد موقع ملف `.lift-ranges` المصاحب له والوسائط). كما تقبل `stats` الخيار `--format json`، حيث تُخرج الأعداد في شكل كائن JSON واحد. + +!!! note + تُعد رموز الخروج الخاصة بـ `validate` ومخطط `--format json` واجهة أتمتة مدعومة: حيث يخضع كلاهما للاختبارات ولا يتغيران إلا وفقًا لمعايير SemVer. + +تقوم الأداة `sort` بإعادة كتابة ملف `.lift` فقط؛ أما الملفات المصاحبة `.lift-ranges` فتُترك كما هي +(يمكن فرزها بشكل منفصل باستخدام واجهة برمجة التطبيقات `RangesFile`). + +تقبل الأوامر `validate` و`stats` و`check-media` و`export` أيضًا حزمة LIFT مضغوطة (ملف `.zip` بأي من الترتيبين — الملفات في جذر الأرشيف، أو متداخلة ضمن مجلد واحد من المستوى الأعلى)؛ حيث يتم استخراجها إلى دليل مؤقت ثم يتم حذفها عند انتهاء تنفيذ الأمر. تستخرج أوامر البث `stats` و`export` ملف `.lift` نفسه فقط، لذا فإنها لا تستهلك موارد كثيرة عند التعامل مع الحزم التي تحتوي على ملفات وسائط كثيرة؛ أما `validate` و`check-media` فتحتاجان إلى المجلد بأكمله وتستخرجان محتوياته بالكامل. + +أمثلة: + +``` +$ sil-lift validate dictionary.lift +خطأ [dangling-ref] dictionary.lift:88 (المدخل apu): المرجع 'nope' لا يتطابق مع أي معرّف مدخل/معرّف فريد (GUID) أو معرّف معنى +تحذير [uri-not-rfc] dictionary.lift:6: : استخدام حرف محرك أقراص Windows كمرجع URI (بأسلوب FLEx file://C:/) +خطأ واحد، تحذير واحد + +$ sil-lift validate dictionary.lift --format json +{ + "problems": [ + { + "level": "error", + "code": "dangling-ref", + "message": "ref 'nope' matches no entry id/guid or sense id", + "file": "dictionary.lift", + "entry_id": "apu", + "guid": null, + "line": 88 + }, + { + "level": "warning", + "code": "uri-not-rfc", + "message": ": تم استخدام حرف محرك أقراص Windows كمرجع URI (بأسلوب FLEx file://C:/)", + "file": "dictionary.lift", + "entry_id": null, + "guid": null, + "line": 6 + } + ], + "summary": { + "errors": 1, + "warnings": 1 + } +} + +$ sil-lift stats sango.lift +entries: 3507 +senses: 4541 +... + +$ sil-lift export dictionary.lift --langs en,fr -o dictionary.csv +``` + +رموز الخروج: `0` نجاح (يُسمح بوجود تحذيرات، ما لم يتم استخدام الخيار `--strict`)، `1` نتائج (أخطاء في التحقق من الصحة / وسائط مفقودة / تحذيرات عند استخدام الخيار `--strict`)، `2` مدخلات غير قابلة للقراءة. diff --git a/docs/ar/guides/folder-media.md b/docs/ar/guides/folder-media.md new file mode 100644 index 0000000..19a35d5 --- /dev/null +++ b/docs/ar/guides/folder-media.md @@ -0,0 +1,41 @@ +# مجلد LIFT: النطاقات ووسائل الإعلام + +عادةً ما يكون معجم LIFT عبارة عن _مجلد_: ملف `.lift`، وملف واحد أو أكثر من ملفات `.lift-ranges` المصاحبة (ملفات sidecar)، وملفات الوسائط الموجودة في مجلدي `audio/` و`pictures/`. + +## النطاقات + +```python +lex = sil_lift.load("dictionary.lift") # يتم تتبع المرافقات تلقائيًا + +lex.ranges_files # {Path(...): RangesFile} +lex.all_ranges() # عرض {id: Range} المدمج +lex.all_ranges()["grammatical-info"].elements +``` + +تتعامل ميزة «Companion discovery» مع الواقع الفعلي: حيث يتم استخدام `range/@href` يشير إلى ملف موجود؛ تعود روابط href المطلقة المعلقة في FieldWorks `file://C:/...` إلى الاسم الأساسي لـ href الموجود بجوار `.lift`؛ ويتم التقاط العنصر الشقيق التقليدي `.lift-ranges` حتى في حالة عدم وجود أي مرجع له. + +تقوم الدالة `lex.save()` بكتابة ملف `.lift` وجميع الملفات المصاحبة التي يتم تتبعها معًا. يتم حفظ التعديلات التي تُجرى على ملف `RangesFile` في ملفه الخاص؛ أما النطاقات التي لم يتم التعديل عليها فتحتفظ بقيم البايتات الخاصة بها تمامًا. الاستخدام المستقل: + +```python +ranges = sil_lift.RangesFile.load("dictionary.lift-ranges") +ranges.find("grammatical-info") +ranges.sort() +ranges.save() +``` + +قم بتمرير المعلمة `resolve_ranges=False` إلى الدالة `load()` لتخطي عملية اكتشاف المرافقات. + +## وسائل الإعلام + +```python +for ref in lex.media_refs(): # كل عنصر من نوع و + print(ref.kind, ref.href, ref.entry_id) + +lex.missing_media() # المراجع التي لا توجد ملفاتها +``` + +يتم تحديد المسار وفقًا للتنسيق التقليدي: يتم فحص رابط href النسبي كما هو مكتوب (مع توحيد علامات الخط المائل العكسي — حيث يكتب WeSay `pictures\photo with space.png`) وتحت مجلد `audio/` (لوسائط النطق) أو `pictures/` (للرسوم التوضيحية). لا يمكن التحقق من روابط href البعيدة/المطلقة، لذا يتم تخطيها. + +## محتويات المجلد الأخرى + +غالبًا ما يحتوي مجلد LIFT على ملفات لا يقوم sil-lift بنمذجتها — مثل نظام الكتابة LDML الموجود ضمن `WritingSystems/`، وملفات الصوت/الصور الخاصة بموافقة المتحدثين في The Combine الموجودة ضمن `consent/`، وما شابه ذلك؛ تترك الدالتان `load()` و`save()` هذه الملفات دون تغيير، بينما تقوم الدالة [`Lexicon.save_zip()`](lift-export-interop.md) بنقلها حرفياً عند حزم المجلد. diff --git a/docs/ar/guides/large-files.md b/docs/ar/guides/large-files.md new file mode 100644 index 0000000..d2f086f --- /dev/null +++ b/docs/ar/guides/large-files.md @@ -0,0 +1,27 @@ +# الملفات الكبيرة (البث المباشر) + +تقوم دالة `load()` بإنشاء مخطط الكائنات بالكامل. بالنسبة للمعاجم التي يتجاوز حجمها عدة مئات من الميغابايت، تعالج واجهة برمجة التطبيقات (API) الخاصة بالبث مدخلاً واحداً في كل مرة ضمن ذاكرة محدودة — وهو من نفس نوع `Entry`، لذا فإن الكود المكتوب لوضع معين يعمل في الوضع الآخر. + +```python +استيراد sil_lift + +مع sil_lift.open_reader("big.lift") كـ reader: + header = reader.header # تم تحليلها مسبقًا (تسبق السجلات) + لكل سجل في reader: # متكرر كسول من نوع Iterator[Entry] + ... +``` + +```python +باستخدام sil_lift.open_reader("big.lift") كقارئ، و sil_lift.open_writer( + "out.lift", header=reader.header, producer="my-script" +) ككاتب: + لكل إدخال في القارئ: + if not entry.date_deleted: # على سبيل المثال، حذف السجلات القديمة + writer.write(entry) +``` + +ملاحظات: + +- ما ينتجه الكاتب هو بالضبط ما سينتج عنه مُسلسل النص القياسي الكامل لنفس المحتوى — ولا يحدث أي تباين بين الوضعين أبدًا. +- لا يعيد وضع البث استخدام أي بايتات من المصدر: يكون الإخراج دائمًا في صيغته القياسية. لا يتم نقل بقايا LIFT على مستوى الجذر — التعليقات الموجودة بين المدخلات والسمات الخارجة عن المخطط في `` —؛ فالمدخلات والرأس كاملة، بما في ذلك البقايا. +- إذا حدث استثناء داخل كتلة `open_writer`، يُترك الملف في حالة غير مكتملة بشكل واضح (بدون `` ختامي) — فلا يجب أن يبدو المعجم الذي لم يكتمل كتابته بعد وكأنه مكتمل. diff --git a/docs/ar/guides/lift-export-interop.md b/docs/ar/guides/lift-export-interop.md new file mode 100644 index 0000000..271691e --- /dev/null +++ b/docs/ar/guides/lift-export-interop.md @@ -0,0 +1,112 @@ +# إنتاج LIFT متوافق مع المعايير + +هذا الدليل موجه لأي شخص يقوم بكتابة _مُصدِّر_ لـ LIFT — أي كود بلغة برمجة أيًّا كانت، يعمل على تحويل نموذج بيانات تطبيق آخر إلى LIFT 0.13. يؤدي `sil-lift` دورين في هذا العمل: بوابة مطابقة تتحقق من توافق الناتج مع المخطط، بالإضافة إلى الدلالات التي لا يستطيع المخطط التعبير عنها، ومرجعًا للأشكال وقواعد النص التي يجب أن يتبعها الناتج. + +يُعد كتابة لغة LIFT أسهل بكثير من تحليلها: فالمُصدِّر لا يُصدر سوى المجموعة الفرعية من البنيات التي ينتجها نموذجه الخاص، ولا يواجه أبدًا الخيارات الاختيارية الواردة في المواصفات الكاملة. يكمن الصعوبة في التفاصيل — العنصر المصاحب `.lift-ranges`، والنص الخاص بكل نظام كتابة، والمعرّفات الثابتة، وعمليات التهرب في XML — وهذه هي بالضبط ما تكتشفه عمليات الفحص الواردة أدناه. + +## الحزم المضغوطة + +عادةً ما يتم نقل LIFT كملف `.zip` واحد — حيث يقوم كل من FieldWorks و The Combine بالاستيراد والتصدير بهذه الطريقة — لذا فإن `sil-lift` يقرأ ويكتب الحزم المضغوطة مباشرةً، بأي من الترتيبين اللذين يستخدمهما النظام البيئي: الملفات الموجودة في جذر الأرشيف، أو الملفات المتداخلة ضمن مجلد واحد من المستوى الأعلى. + +- **قراءة:** تقوم `sil_lift.load("package.zip")` باستخراج الملفات إلى دليل مؤقت، وتحديد موقع ملف `.lift` الوحيد، وتحميله (يتم تحليل الملفات المصاحبة والوسائط كالمعتاد). + - تقبل أوامر واجهة سطر الأوامر `validate` و`stats` و`check-media` و`export` مسار ملف `.zip` أيضًا، لذا فإن البرنامج التالي يعمل على الحزمة كما هي. + - دفقَي `stats` و`export`، واستخراج ملف `.lift` فقط بدلاً من الحزمة بأكملها — بحيث تظل عملية الاستخراج خفيفة في الحزم التي تحتوي على الكثير من الوسائط، ويُطبق حد الاستخراج على ملف `.lift` وحده وليس على كل ما عدا ذلك. + - يُحدد الحد الأقصى لعملية الاستخراج بـ 10 جيجابايت و100,000 عنصر؛ ويتم رفض أي حزمة تتجاوز أيًّا من هذين الحدين مع ظهور خطأ `LiftParseError`، وكذلك الحزمة التي تتجاوز مسارات عناصرها حدود دليل الاستخراج. +- **اكتب:** `Lexicon.save_zip("out.zip", wrap_folder="MyDict")` يقوم بتجميع ملف `.lift` وملفات `.lift-ranges` الخاصة به، بالإضافة إلى كل الملفات الأخرى الموجودة في المجلد المصدر (الوسائط، `WritingSystems/`، `consent/`، ...) في ملف مضغوط. + - يتم تعيين `wrap_folder` افتراضيًّا على مجلد من المستوى الأعلى يحمل اسم ملف الـ zip (وفقًا لقواعد الاستيراد في FieldWorks/Combine)؛ قم بتمرير `False` للحصول على أرشيف مسطح. + +يحتفظ كل من `.lift` و`.lift-ranges` بدقة البايت داخل الحزمة؛ أما حاوية zip نفسها فهي غير قابلة لإعادة إنتاج البايتات. + +## التحقق من صحة المخرجات كمرحلة للتأكد من المطابقة + +وجه الأمر `sil-lift validate` إلى ملف `.lift` الذي تم إنشاؤه. وهو يُشغّل RELAX NG (على كل من `.lift` وملف `.lift-ranges` المصاحب له) بالإضافة إلى إجراء فحوصات دلالية لا يمكن للقواعد النحوية التعبير عنها: المراجع المعلقة لـ `relation`/`variant`، ومعرّفات GUID المكررة، وسلامة العنصر الأصلي في النطاق، وقيم السمات والمعلومات النحوية غير المُعرَّفة في نطاقها، ومراجع الرأس `range/@href` التي لا تُحل إلى أي عنصر مرافق. + +بالنسبة لـ CI، في حالة الفشل في أي خطوة، يجب إصدار نتائج قابلة للقراءة آليًّا: + +``` +sil-lift validate export.lift --strict --no-check-media --format json +``` + +- يؤدي الخيار `--strict` إلى إيقاف عملية التشغيل في حالة ظهور تحذيرات (وليس الأخطاء فقط). +- تتخطى الخيار `--no-check-media` عملية التحقق من وجود الوسائط في نظام الملفات، حيث تُعد نتائج `missing-media` التي تظهرها هذه العملية مجرد تشويش عندما لا تكون ملفات الصوت أو الصور موجودة في نفس المجلد الذي يوجد فيه ملف `.lift` في بيئة التكامل المستمر (CI). +- يُخرج الأمر `--format json` كائن JSON واحد (`{"problems": [...], "summary": {...}}`) بدلاً من النص المقروء؛ وتشكل رموز الخروج الخاصة به ومخططه واجهة مدعومة وخاضعة لمعايير SemVer (انظر [دليل سطر الأوامر](cli.md)). +- تُظهر `--require-ids` أخطاءً إضافيةً في حالة وجود إدخالات تفتقد إلى `guid` أو في حالة وجود إدخالات تفتقد إلى `id` — وهو أمر مفيد عندما يتعين عند إعادة الاستيراد لاحقًا تحديث البيانات بدلاً من تكرارها. + +احذر من فقدان البيانات الخفي (نمط الفشل الذي يجعل التصدير إلى ملف CSV عادي مصحوبًا بفقدان البيانات) عن طريق التحقق من الأعداد باستخدام الأمر `stats --format json` على نموذجك المصدر: + +``` +تصدير إحصائيات sil-lift.lift --format json +``` + +ويقدم التقرير إحصاءات عن «المدخلات» و«المعاني» و«الأمثلة» و«مراجع الوسائط» و«اللغات»، بالإضافة إلى «السمات» لكل اسم على حدة. + +### تشغيل البرنامج دون استخدام سلسلة أدوات Python + +يمكن لنظام التكامل المستمر (CI) لمشروع TypeScript أو C# إجراء نفس الفحص دون تثبيت Python، وذلك عبر GitHub Action المدمج: + +```yaml +- الاستخدامات: sillsdev/python-sil-lift@v0.1.0 + مع: + المسار: export.lift + الإعداد الصارم: "true" + عدم التحقق من الوسائط: "true" + التنسيق: json +``` + +أو صورة الحاوية، التي تم إنشاؤها من ملف `Dockerfile` الموجود في المستودع: + +``` +docker build -t sil-lift . +docker run --rm -v "$PWD:/work" -w /work sil-lift validate export.lift --strict +``` + +## المكون المصاحب `.lift-ranges` + +توجد المفردات الخاضعة للرقابة — مثل أجزاء الكلام، والمجالات الدلالية، وأي مجموعة قيم أخرى مرتبة حسب السمات — في ملف `.lift-ranges` موازٍ، يُشار إليه من ملف `
`: + +```xml +
+ + + + +
+``` + +يحتوي الدليل المصاحب على التعريف الكامل لكل نطاق. القيم هي ``؛ حيث تُشكّل `parent` تسلسلاً هرميًّا؛ أما `label` / `abbrev` / `description` فهي نصوص متعددة: + +```xml + + + + + +
n
+
+
+ + + + + +
+``` + +ثم يشير المدخل إلى قيمة ما باستخدام المعرّف: ففئة الجزء من الكلام هي ``، والمجال الدلالي هو ``. تُصدر `sil-lift validate` تحذيرًا (`undefined-range-value`) عندما تكون القيمة غير محددة ضمن نطاقها، وتُصدر خطأً (`range-parent`) عندما لا يكون `parent` معرّفًا لأحد العناصر المتجاورة — لذا، قم بتحديد النطاقات التي تستخدمها بياناتك فعليًّا. انظر أيضًا [النطاقات ووسائل الإعلام](folder-media.md). + +إذا قمت بإنشاء عملية التصدير باستخدام لغة Python، فإن الدالات `Lexicon.add_ranges_file()` و`RangesFile.add_range()` و`Range.add_element()` تقوم بإنشاء الملف المصاحب وإضافة مراجع الرؤوس نيابة عنك؛ وتقوم الدالة `open_writer(..., ranges=...)` بنفس الشيء على مسار التدفق. + +## نص ونصوص متعددة + +كل سلسلة نصية بلغة بشرية في LIFT هي _نص متعدد_: حيث يوجد `
` واحد لكل نظام كتابة، ويحتوي كل منها على ``: + +```xml + + كانغا +
الدجاجة
+
+``` + +يتم تعيين النموذج الذي يستخدم رموز اللغات كمفاتيح للسلاسل (مثل `MultiString`، أو `Record`، أو `dict[str, str]`) وفقًا لعلاقة «واحد إلى واحد»: حيث يتحول كل إدخال لكل مفتاح إلى عنصر واحد من نوع `
`. يُسمح بوجود نموذج واحد على الأكثر لكل لغة في نص متعدد واحد — وإلا فإن `sil-lift` ستصدر تحذيرًا من نوع `duplicate-form-lang`. + +يُعد «الهروب من أحرف XML» الجزء الوحيد الذي يتطلب دقة فائقة حقًّا. في نص العنصر، يجب استخدام أحرف الهروب لـ `&` و`<`, and `>` (`&`، `<`، `>`)؛ أما في قيم السمات، فيجب استخدام أحرف الهروب لـ `"` أيضًا. يطبق مطور `sil-lift` هذه القواعد بالضبط ولا يغير أبدًا المسافات البيضاء داخل `` — فهو لا يضيف أي تباعد هناك، لأن ذلك من شأنه إتلاف البيانات اللغوية. إذا كنت تهدف إلى مطابقة ناتجه، فاستخدم آلية الهروب الخاصة بمُسلسل XML حقيقي (وليس عملية استبدال مبرمجة يدويًّا تتجاهل الرمز `&`) واحتفظ بمحتوى `` بايتًا بايتًا تمامًا كما هو موجود في المصدر. diff --git a/docs/ar/guides/read-edit-write.md b/docs/ar/guides/read-edit-write.md new file mode 100644 index 0000000..709737e --- /dev/null +++ b/docs/ar/guides/read-edit-write.md @@ -0,0 +1,66 @@ +# القراءة، التحرير، الكتابة + +## جاري التحميل + +```python +import sil_lift + +lex = sil_lift.load("dictionary.lift") +``` + +تقبل الدالة `load()` أي مستند LIFT **0.13** صحيح التكوين — بما في ذلك الملفات الواقعية التي لا تتوافق مع المخطط. يتم نقل أي شيء لا يحدده النموذج (العناصر/السمات غير المعروفة، التعليقات) دون أي فقدان للبيانات باعتباره «بقايا LIFT» في الحقل غير الشفاف `extra` لكل عقدة. أما الإصدارات الأخرى من LIFT، فتُحدث استثناءً من نوع `LiftParseError` مع ذكر اسم الإصدار. + +## النموذج + +كل عنصر في LIFT هو فئة بيانات محددة النوع: `Entry`، `Sense`، `Example`، `Pronunciation`، `Variant`، `Relation`، `Etymology`، `Reversal`، وهكذا دواليك. النص متعدد اللغات هو كائن من نوع `Multitext`، والذي يعمل كخريطة تربط رمز اللغة بكائن من نوع `Text`: + +```python +entry = lex.find(id="abat") + +str(entry.lexical_unit["seh"]) # "abat" +entry.lexical_unit["en"] = "grove" # يتم تحويل السلاسل العادية +"en" in entry.citation # False +``` + +يتم تنظيم `Text` — كقائمة مرتبة من أجزاء `str` و`Span` — لأن `` يمكن أن تحتوي على علامات `` متداخلة. يُحول `str(text)` النص إلى نص عادي؛ بينما تحتفظ الأجزاء بعلامات الترميز لضمان إمكانية العودة إلى الصيغة الأصلية. + +تكون «التفسيرات» _على شكل صيغ_ في LIFT (حيث يحمل كل `` لغته الخاصة)، لذا فإن «المعنى» يحتوي على `glosses: list[Form]` بالإضافة إلى دالة مساعدة: + +```python +sense = entry.senses[0] +sense.gloss("en") # Text | None +entry.gloss_langs() # {"en", "id"} +``` + +## الحفظ + +```python +lex.save() # العودة إلى المكان الذي تم التحميل منه +lex.save("elsewhere.lift") +``` + +يتم إعادة كتابة الإدخالات التي لم تقم بتعديلها **بشكل مطابق تمامًا على مستوى البايت**؛ أما المستند الذي لم تقم بتعديله على الإطلاق، فهو مطابق تمامًا على مستوى البايت من البايت الأول وحتى الأخير. انظر [ضمانات فيديليتي](../fidelity.md) للاطلاع على نص العقد الدقيق. + +## البناء من الصفر + +```python +lex = sil_lift.Lexicon(producer="my-script 1.0") +entry = sil_lift.Entry(id="hello", guid="...") +entry.lexical_unit["en"] = "hello" +sense = sil_lift.Sense() +sense.glosses.append(sil_lift.Form("fr", sil_lift.Text(["bonjour"]))) +entry.senses.append(sense) +lex.entries.append(entry) +lex.save("new.lift") +``` + +## الترتيب القياسي + +```python +lex.sort() # ترتيب الإدخالات حسب (guid، id)؛ النطاقات/تعريفات الحقول حسب id/tag +lex.save() # تحتفظ الإدخالات التي لم يتم التعديل عليها ببايتاتها بالضبط، بالترتيب الجديد + +sil_lift.canonicalize("in.lift", "out.lift") # أعيد تسلسلها بالكامل، وجاهزة للمقارنة +``` + +انظر أيضًا: [مثال توضيحي: التحرير الجماعي للتعليقات التوضيحية](bulk-edit-glosses.md). diff --git a/docs/ar/guides/validate.md b/docs/ar/guides/validate.md new file mode 100644 index 0000000..4131e15 --- /dev/null +++ b/docs/ar/guides/validate.md @@ -0,0 +1,36 @@ +# التحقق من الصحة + +يكون التحقق من الصحة دائمًا صريحًا — فلا يتم التحقق من صحة عمليات التحميل والحفظ أبدًا بشكل ضمني. + +```python +import sil_lift + +# شامل: دفق كسول من المشكلات (المخطط + الطبقات الدلالية). +for problem in sil_lift.iter_problems("dictionary.lift"): + print(problem) + # خطأ [dangling-ref] dictionary.lift:88 (entry apu): المرجع 'nope' يطابق ... + +# الفشل السريع: يثير استثناء LiftValidationError عند أول مشكلة على مستوى الخطأ. +sil_lift.validate_file("dictionary.lift") + +# الحالة في الذاكرة (يتم التسلسل أولاً — تكلفة موثقة في المعاجم الكبيرة): +lex = sil_lift.load("dictionary.lift") +problems = list(lex.iter_problems()) +``` + +تحتوي كل «مشكلة» على «مستوى» («خطأ»/«تحذير») و«رمز» ثابت و«رسالة» وعنوان: «ملف»، «معرف_المدخل»، «معرف_فريد»، «سطر». + +## الطبقات + +1. **RELAX NG** وفقًا لقواعد النحو LIFT 0.13 (مأخوذة من lift-standard — وهي نسخة مطابقة تمامًا من حيث البايتات تم تضمينها في هذه الحزمة). +2. **مخطط النطاقات** — ملف `lift-ranges-0.13.rng` الخاص بهذا المشروع — على كل مرفق `.lift-ranges` يتم تتبعه. +3. **التحققات الدلالية** التي لا يمكن للقواعد النحوية التعبير عنها: `duplicate-guid`، `dangling-ref`، `range-parent`، `undefined-range-value`، `duplicate-form-lang`، `missing-media`. + +## نتائج برنامج FieldWorks (FLEx) في الواقع الميداني + +يقوم برنامج FieldWorks بشكل منهجي بكتابة بعض المحتوى الذي ترفضه الأدوات الصارمة. فيما يلي سياسة «sil-lift»، بحيث يتم التحقق من صحة المعاجم الحقيقية بشكل مفيد: + +- يتم الإبلاغ عن روابط href من النوع `file://C:/...` (عناوين URI غير صالحة) على أنها **تحذيرات** (`uri-not-rfc`)، وليست أخطاء في المخطط — ولم يرفضها أداة التحقق من صحة C# مطلقًا. +- لا يتم وضع علامة على العناصر الفرعية المتداخلة بشكل قانوني (مثل `field, note, field, note` بمعنى ما)، وذلك لتجنب حدوث نتيجة إيجابية خاطئة في مكتبة libxml2. +- تتم مقارنة قيم النطاقات وفقًا لتوحيد Unicode NFC — حيث يقوم FLEx بكتابة ملف `.lift` بتنسيق NFC، بينما يكتب ملف `.lift-ranges` بتنسيق NFD ضمن نفس التصدير. +- يتم الإبلاغ عن امتدادات `trait`/`field` الخاصة بـ FLEx داخل `range-element` \*\*(\*\*أخطاء في المخطط مقابل مخطط النطاقات): فهي انحرافات حقيقية عن المواصفات. diff --git a/docs/ar/index.md b/docs/ar/index.md new file mode 100644 index 0000000..5a1bd6a --- /dev/null +++ b/docs/ar/index.md @@ -0,0 +1,32 @@ +# سيل-ليفت + +مكتبة Python لـ [LIFT](https://github.com/sillsdev/lift-standard) (Lexicon Interchange FormaT) 0.13: قراءة/كتابة بدون فقدان البيانات لمجلد LIFT (`.lift` + `.lift-ranges` + مراجع الوسائط)، والتحقق من صحة المخطط والسمات الدلالية، والفرز القياسي — مع واجهات برمجة تطبيقات (APIs) للتدفق المباشر للمعاجم الكبيرة. + +**الحالة: إصدار تجريبي، قيد التطوير النشط.** + +## تثبيت + +من [PyPI](https://pypi.org/project/sil-lift/): + +``` +pip install sil-lift # المكتبة + الأمر sil-lift +``` + +يتطلب إصدار Python 3.11 أو أحدث. التبعية الوحيدة في وقت التشغيل هي lxml. + +## جولة مدتها 30 ثانية + +```python +import sil_lift + +lex = sil_lift.load("thesaurus.lift") # يتتبع أيضًا المرادفات ذات النطاقات .lift + +for entry in lex.entries: + if "en" not in entry.gloss_langs(): + print(entry.id, str(entry.lexical_unit.get("seh") or "")) + +entry = lex.find(guid="0f5a9c3e-...") # أو lex.find(id="hoofd_a1b2") +entry.senses[0].definition["en"] = "head (anatomy)" + +lex.save() # المدخلات التي لم يتم تعديلها متطابقة من حيث البايتات؛ أما المدخلات التي تم تعديلها فقد أعيد تسلسلها +``` diff --git a/docs/ar/reference.md b/docs/ar/reference.md new file mode 100644 index 0000000..714761a --- /dev/null +++ b/docs/ar/reference.md @@ -0,0 +1,9 @@ +# مرجع واجهة برمجة التطبيقات (API) + +واجهة برمجة التطبيقات العامة (API) هي بالضبط ما يعيد `sil_lift` تصديره على المستوى الأعلى؛ أما الوحدات النمطية التي تبدأ بعلامة التسطير السفلي فهي تفاصيل تتعلق بالتنفيذ. + +::: sil_lift +options: +show_root_heading: false +show_source: false +members_order: source diff --git a/docs/de/csharp-differences.md b/docs/de/csharp-differences.md new file mode 100644 index 0000000..57aa6f0 --- /dev/null +++ b/docs/de/csharp-differences.md @@ -0,0 +1,52 @@ +# Unterschiede zu den C#-Bibliotheken + +sil-lift ist in etwa vergleichbar mit den C#-LIFT-Tools von SIL – vor allem mit `SIL.Lift` in [libpalaso](https://github.com/sillsdev/libpalaso) (Parser, Validator, Migrator, `LiftSorter`), `SIL.DictionaryServices` im selben Repository (das `LexEntry`/`LexSense`-Modell mit eigenem LIFT-Reader/Writer, das von The Combine und WeSay verwendet wird) sowie die LIFT-Handler in [Chorus](https://github.com/sillsdev/chorus). Es handelt sich um eine neue Implementierung, nicht um eine Portierung. Auf dieser Seite wird zusammengefasst, in welchen Punkten sich das Verhalten bewusst unterscheidet. + +## Geltungsbereich + +| Fähigkeit | C#-Bibliotheken | sil-lift | +| ----------------------------------------- | --------------------------------------------------- | --------------------------------------------------------------------------------------------------- | +| LIFT-Versionen | 0,10–0,13 (Migration integriert) | **nur 0.13**; ältere Versionen werden mit einer eindeutigen Fehlermeldung abgelehnt | +| Versionsmigration | `Migrator` (XSLT-Kette) | keine — Verwenden Sie die XSLT-Dateien aus „lift-standard“ für einmalige Upgrades | +| 3-Wege-Zusammenführung / Synchronisierung | Refrain | fällt nicht in den Geltungsbereich | +| Validierung | Nur RELAX NG (`Validator`) | RELAX NG + Schema- und Semantikprüfungen | +| Streaming | interne Analyse auf Eintragsebene | Öffentliche `open_reader`-/`open_writer`-API | + +## API-Form + +Der Parser von `SIL.Lift` ist callback-gesteuert (`ILexiconMerger`): Er übergibt Parsing-Ereignisse an einen Verbraucher. sil-lift gibt stattdessen einen einfachen Objektgraphen zurück – typisierte Dataklassen für jedes LIFT-Element –, da Python-Skriptentwickler Objekte und keine Callbacks wünschen. `SIL.DictionaryServices` legt zwar ein `LexEntry`/`LexSense`-Objektmodell über `SIL.Lift“, doch als Anwendungsmodell repräsentiert es lediglich die Konstrukte, die diese Anwendungen verwenden – daher kann bei einer erneuten Serialisierung über dieses Modell hinweg der Inhalt außerhalb des Modells nicht in derselben Weise erhalten bleiben, wie dies bei der Behandlung von LIFT-Resten und der Byte-Genauigkeit von sil-lift der Fall ist (siehe unten). Die Streaming-API liefert denselben `Entry\`-Typ, sodass kein zweites, vereinfachtes Modell erlernt werden muss. + +## Round-Trip-Genauigkeit + +Der deutlichste bewusste Unterschied. Beim Speichern mit `SIL.Lift` wird das gesamte Dokument erneut serialisiert. sil-lift garantiert: + +- Ein unverändertes Dokument wird **byte-identisch** gespeichert, und +- Unveränderte Einträge behalten ihre exakten Quellbytes bei, auch wenn sich andere Einträge ändern – es handelt sich um dieselbe, pro Eintrag erfolgende Byte-Aufteilung, die Chorus verwendet und die automatisch angewendet wird. + +Siehe [Garantien zur Richtigkeit](fidelity.md). + +## Validierung + +Der C#-`Validator` führt einen RELAX-NG-Durchlauf durch und gibt die ersten Fehler als Zeichenfolgen zurück. sil-lift meldet einen strukturierten `Problem`-Stream, wobei jeder Eintrag die betreffende Datei, den Eintrag und die Zeile enthält, und seine Schemaebene weicht bewusst an drei Stellen ab: + +- **Ungültige URIs sind Warnungen, keine Fehler.** Die C#-RELAX-NG-Engine hat den Datentyp `anyURI` nie erzwungen, weshalb FieldWorks (FLEx) seit Jahren `file://C:/...`-href-Links in echte Lexika schreibt. Würden diese Dateien abgelehnt, würde dies praktisch jeden FLEx-Export als fehlerhaft kennzeichnen. +- **Schematron-Regeln werden durchgesetzt** (als semantische Prüfungen): Doppelte Formularsprachen und ähnliche Ko-Einschränkungen in der LIFT-Grammatik wurden sowohl bei der C#-Validierung als auch bei der reinen lxml-Validierung stillschweigend ignoriert. +- **Dateiübergreifende Vergleiche sind Unicode-normalisiert**, da FLEx die `.lift`-Datei in NFC und die zugehörige `.lift-ranges`-Datei in NFD schreibt. + +sil-lift validiert außerdem die `.lift-ranges`-Begleitdateien eines geladenen Lexikons anhand eines Schemas für eigenständige Bereichsdokumente (das zusammen mit der LIFT-Basisgrammatik aus `lift-standard` bereitgestellt wird) — jede nachverfolgte externe Bereichsdatei wird bei jeder Validierung der `.lift`-Datei überprüft — während es in der C#-Welt kein solches Schema (oder keine solche Überprüfung) gibt. (Es gibt keine Möglichkeit, eine `.lift-ranges`-Datei für sich allein, losgelöst von einer `.lift`-Datei, zu validieren.) + +## Kanonische Sortierung + +`Lexicon.sort()` spiegelt die Kernregeln von `LiftSorter` wider (Einträge nach GUID ohne Berücksichtigung der Groß-/Kleinschreibung; Bereiche und Bereichselemente nach ID; Definitionen von Header-Feldern nach Tag; Bedeutungen werden in der Reihenfolge der Datei beibehalten; Leerzeichen innerhalb von `` werden nie verändert), mit drei Unterschieden: + +- Einträge ohne GUID werden deterministisch nach ID sortiert (`LiftSorter` geht davon aus, dass eine GUID vorhanden ist); +- Die Sortierreihenfolge ist lokalisierungsunabhängig (reine, groß-/kleinschreibungsunabhängige Codepunkte, nicht die .NET-Sortierreihenfolge für kulturunabhängige Sortierung); +- Listen desselben Typs wie Notizen, Beziehungen und Formulare behalten ihre Reihenfolge im Dokument bei, anstatt nach dem Schlüssel neu sortiert zu werden – die Gruppierung ist bereits deterministisch, und eine Neuanordnung würde nur unnötiges Rauschen verursachen. + +Die Datei `canonicalizeLift.xsl` aus dem Spec-Repo wird überhaupt nicht verwendet: Sie komprimiert Leerzeichen innerhalb des lexikalischen Texts (destruktiv) und die von ihr generierten IDs unterscheiden sich bei jedem Durchlauf. + +## Nicht übernommen + +- WeSay-spezifische Funktionen (Dashboard/Konfiguration im Zusammenhang mit LIFT-Dateien). +- `SynchronicMerger` (Zusammenführung von Chorus-Updates) – das Konzept der Byte-Chunks lebt in der Fidelity-Schicht weiter, die Zusammenführung hingegen nicht. +- LDML-Schriftsystem-Analyse: Dateien im Verzeichnis `WritingSystems/` werden als undurchsichtiger Ordnerinhalt behandelt. diff --git a/docs/de/fidelity.md b/docs/de/fidelity.md new file mode 100644 index 0000000..f01deb2 --- /dev/null +++ b/docs/de/fidelity.md @@ -0,0 +1,33 @@ +# Fidelity-Garantien + +LIFT ist ein _Austauschformat_, daher lautet die erste Regel: **Lass niemals etwas weg, was du nicht verstehst**. Der Vertrag von `sil-lift`, der bei jedem Durchlauf durch die Testsuite überprüft wird (Korpusdateien sowie eigenschaftsbasierte Generierung): + +## Lesen + +Jedes wohlgeformte LIFT 0.13-Dokument wird geladen – auch Inhalte, die nicht dem Schema entsprechen. Alles, was das Modell nicht definiert, wird im opaken `Extras`-Container des nächstgelegenen Knotens als _LIFT-Residuum_ gespeichert – so nennt FieldWorks dieses Konzept, das in einem `LiftResidue`-Feld abgelegt wird: unbekannte Attribute und Elemente, XML-Kommentare und Verarbeitungsanweisungen, verstreuter Text sowie fehlerhaft formatierte typisierte Attribute (ein fehlerhaftes Datum verbleibt als ursprüngliche Zeichenkette in `Extras`; das typisierte Feld ist `None`). + +## Ein unverändertes Dokument speichern + +`load()` → `save()` ohne Änderungen erzeugt eine **byte-identische Ausgabe** – keine Neuformatierung, kein erneutes Escaping, keine Neuanordnung, einschließlich Byte-Order-Markierungen und XML-Deklarationen. Derzeit gibt es keine Normalisierungsliste: Die Identität ist exakt. + +Ausnahmen (der Writer greift auf die vollständige kanonische Serialisierung zurück, die zwar semantisch vollständig, jedoch nicht bytekonsistent ist): + +- die Quellkodierung ist nicht ASCII-kompatibel (nicht UTF-8/US-ASCII) oder +- der Quelltext enthält einen DOCTYPE oder +- Der Byte-Scanner und der Parser sind sich hinsichtlich der obersten Struktur des Dokuments nicht einig – beispielsweise bei einem nicht spezifikationskonformen zweiten `
`, den der Parser nur einmal beibehält (der Scanner ist bewusst konservativ: Im Zweifelsfall erfasst er überhaupt keine Quell-Bytes), oder +- Der Quellcode wurde im Speicher erstellt und nicht aus einer Datei geladen. + +## Ein bearbeitetes Dokument speichern + +- **Unveränderte Einträge werden wortwörtlich aus ihren ursprünglichen Bytes ausgegeben.** Ein Eintrag gilt als verändert, wenn sich seit dem Parsen ein beliebiger Teil seines Modellobjekts geändert hat (erkannt anhand eines Snapshots der kanonischen Serialisierung, nicht anhand eines „Dirty“-Flags). +- **Die bearbeiteten Einträge werden kanonisch und vollständig neu serialisiert**: UTF-8, Einrückung mit zwei Leerzeichen _außerhalb_ von gemischtem Inhalt (Leerzeichen innerhalb von `` und `` werden niemals verändert), eine dokumentierte Untergruppierung pro Element (z. B. Eintrag: lexikalische Einheit, Zitat, Aussprachen, Varianten, Bedeutungen, Anmerkungen, Beziehungen, Etymologien, Annotationen, Merkmale, Felder), feste Attributreihenfolge, Datumsangaben nach ISO-8601 (`Z` für UTC). Alle Reste werden erneut ausgegeben; ihre Position wird auf den ursprünglichen Unterindex zurückgesetzt und an die neue Unterliste angepasst (eine Annäherung – exakte Byte-Positionen sind nur für unveränderte Einträge garantiert). +- Das Hinzufügen, Entfernen oder Neuanordnen von Einträgen führt zu einer erneuten Serialisierung der Dokumentstruktur, wobei die Bytes jedes unveränderten Eintrags jedoch unverändert ausgegeben werden. + +!!! note ""Die Canonical-" -Datei hier steht in keinem Zusammenhang mit anderen Canonical-XML-Dateien." + Unter „kanonischer Form“ wird auf dieser Seite die in einem der obigen Aufzählungspunkte beschriebene, von `sil-lift` selbst dokumentierte Form verstanden. Dies steht in keinem Zusammenhang mit dem „Canonical XML (C14N)“-Prozess des W3C. Es steht in keinem Zusammenhang mit der Klasse `CanonicalXmlSettings` von `SIL.Core`. + +## Bekannte Näherungswerte (nur berührte Knoten) + +- Kommentare _innerhalb_ eines ``-Laufs bleiben erhalten, werden jedoch neben den Lauf verschoben und nicht an ihrer genauen Zeichenposition beibehalten. +- Die kreuzförmige Reihenfolge der untergeordneten Elemente innerhalb eines bearbeiteten Elements wird auf die kanonische Gruppierung normiert (durch das `interleave`-Schema des LIFT-Schemas wird diese Reihenfolge semantisch irrelevant). +- Ein Multitext-Element, das zwar vorhanden ist, aber nichts enthält – weder Formen noch Reste, z. B. `` – wird nicht erneut ausgegeben. Das Modell stellt diese Felder als stets vorhandenes `Multitext` dar (`lexical-unit`, `citation`, `definition`, die `usage` einer Relation sowie `label` / `abbrev` / `description` bei URL-Verweisen, Bereichen, Bereichselementen und der Kopfzeile) dar, sodass sich ein leeres Feld nach der Analyse nicht von einem fehlenden unterscheiden lässt. Es geht keine semantische Information verloren. diff --git a/docs/de/guides/build-export.md b/docs/de/guides/build-export.md new file mode 100644 index 0000000..9c0f08f --- /dev/null +++ b/docs/de/guides/build-export.md @@ -0,0 +1,168 @@ +# Beispiel: Erstellung eines LIFT-Exports von Grund auf + +Wenn Sie die Daten einer anderen Anwendung als LIFT exportieren – die Aufgabe, die hinter [Erstellung konformer LIFT-Dateien](lift-export-interop.md) steht –, kann `sil-lift` das Dokument Objekt für Objekt aufbauen und serialisieren, anstatt XML manuell zu generieren. Hier wird ein Skript Schritt für Schritt erläutert, das einen Eintrag mit den Bestandteilen eines echten Wörterbuchs erstellt (mehrere Schriftsysteme, eine Aussprache, eine Bedeutung mit einem Beispiel, eine Illustration, ein Merkmal des semantischen Bereichs und ein anwendungsspezifisches Feld), die kontrollierten Vokabulare in eine zugehörige `.lift-ranges`-Datei schreibt, die Daten validiert und speichert. + +## Das Drehbuch + +```python +from pathlib import Path + +import sil_lift + +lex = sil_lift.Lexicon(producer="my-exporter") + +# Ein Eintrag, der aus dem Quellmodell erstellt wurde. +entry = sil_lift.Entry(id="kanga", guid="6b9e7c2a-3f4d-4a1b-8c5e-2d9f0a1b2c3d") +entry.lexical_unit["seh"] = "nkhuku" +entry.lexical_unit["pt"] = "galinha" + +pron = sil_lift.Pronunciation() +pron.forms["en"] = "Sprecher: Ana" # Die Konvention des Combine für Sprecherbezeichnungen +pron.media.append(sil_lift.URLRef(href="audio/nkhuku.wav")) +entry.pronunciations.append(pron) + +sense = sil_lift.Sense(id="kanga_s1") +sense.grammatical_info = sil_lift.GrammaticalInfo(value="Noun") +sense.glosses.append(sil_lift.Form(lang="en", text=sil_lift.Text(["chicken"]))) +sense.definition["en"] = "ein Hausgeflügel, das wegen seiner Eier und seines Fleisches gehalten wird" + +example = sil_lift.Example() +example.forms["seh"] = "Ndinafuna nkhuku." +translation = sil_lift.Translation() +translation.forms["en"] = "I want a chicken." +example.translations.append(translation) +sense.examples.append(example) + +photo = sil_lift.URLRef(href="pictures/hen.jpg") +photo.label["en"] = "Eine Henne" +sense.illustrations.append(photo) + +sense.traits.append(sil_lift.Trait(name="semantic-domain-ddp4", value="1.6.1.2")) + +scientific = sil_lift.Field(type="scientific-name") # ein anwendungsspezifisches Zusatzfeld +scientific.content["en"] = "Gallus gallus domesticus" +sense.fields.append(scientific) + +entry.senses.append(sense) +lex.entries.append(entry) + +# Die kontrollierten Vokabulare, auf die sich der Eintrag bezieht, in einer zugehörigen .lift-ranges-Datei. +ranges = sil_lift.RangesFile() +ranges.add_range("grammatical-info").add_element("Noun").label["en"] = "noun" +ranges.add_range("semantic-domain-ddp4").add_element("1.6.1.2").label["en"] = "Bird" +lex.add_ranges_file(ranges, href="birds.lift-ranges") + +# Überprüfe, was save() schreiben würde, bevor die Datei auf die Festplatte geschrieben wird. +problems = list(lex.iter_problems()) +print(f"validation: {len(problems)} Problem(e)") + +out = Path("export") +out.mkdir(exist_ok=True) +lex.save(out / "birds.lift") +print("=== birds.lift ===") +print((out / "birds.lift").read_text(encoding="utf-8"), end="") +print("=== birds.lift-ranges ===") +print((out / "birds.lift-ranges").read_text(encoding="utf-8"), end="") +``` + +## Was dabei entsteht + +`Validierung: 0 Problem(e)`, dann das `.lift` und sein Begleitelement nebeneinander: + +``` +=== birds.lift === + + +
+ + + + +
+ + + + nkhuku + +
+ galinha +
+
+ +
+ Speaker: Ana +
+ +
+ + + + chicken + + +
+ a domestic fowl kept for its eggs and meat +
+
+ +
+ Ndinafuna nkhuku. +
+ +
+ I want a chicken. +
+
+
+ + + + + +
+ Gallus gallus domesticus +
+
+
+
+
+=== birds.lift-ranges === + + + + + + + + + + + + + +``` + +## Hinweise zur API + +- Multitext-Felder (`lexical_unit`, `definition`, eine `Form`/`URLRef`-Bezeichnung, der Inhalt eines `Field`s, ...) Über die Mapping-Schnittstelle wird pro Schriftsystem eine Zeichenfolge übernommen: `entry.lexical_unit["seh"] = "nkhuku"` fügt ein `
` hinzu. Ein Quellmodell, das Zeichenfolgen anhand des Sprachcodes zuordnet, lässt sich direkt darauf abbilden. +- `RangesFile.add_range()` / `Range.add_element()` erstellen die kontrollierten Vokabulare, und `Lexicon.add_ranges_file(ranges, href=...)` fügt das zugehörige Dokument hinzu und ergänzt die Header-Referenzen `` – sodass die Einträge `` und `` auf die von Ihnen definierten Bereiche verweisen. +- Ein `URLRef` besteht aus einem `href` sowie einem optionalen Multitext für eine Beschriftung oder Bezeichnung – er wird sowohl für `` (Audio) als auch für `` (Fotos) verwendet. Die Aussprache folgt hier der Konvention von „The Combine“, wonach die „en“-Form wie folgt ausgesprochen wird: „Sprecher: “. +- App-spezifische Daten ohne native LIFT-Heimfahrten als „ “ (oder „ “): FieldWorks interpretiert diese als benutzerdefinierte Felder, und The Combine behält sie bei. +- Weisen Sie jedem Eintrag eine echte, stabile `guid` zu (z. B. aus `uuid.uuid4()`, die bei allen Exporten wiederverwendet wird) – bei einem späteren Reimport wird der Eintrag an Ort und Stelle aktualisiert, anstatt ihn zu duplizieren. `sil-lift validate --require-ids` sorgt dafür, dass dies eingehalten wird. +- `lex.iter_problems()` überprüft das Dokument im Arbeitsspeicher (das, was `save()` schreiben würde), bevor Daten auf die Festplatte geschrieben werden; hier ist es fehlerfrei. Da das Lexikon noch keinen Ordner hat, werden die Überprüfungen auf Medienpräsenz und Companion-Href übersprungen – führen Sie [`sil-lift validate`](cli.md) auf der gespeicherten Ausgabe aus (oder mit `--no-check-media`), sobald die Audio- und Fotodateien vorhanden sind. + +## Verpackung + +`lex.save("export/birds.lift")` schreibt die Ordnerstruktur (`.lift` + `.lift-ranges` nebeneinander). Um ein einzelnes ZIP-Paket zu erstellen, das von FieldWorks und The Combine direkt importiert werden kann, verwenden Sie stattdessen `lex.save_zip("birds.zip")` – siehe [Erstellung konformer LIFT-Dateien](lift-export-interop.md). diff --git a/docs/de/guides/bulk-edit-glosses.md b/docs/de/guides/bulk-edit-glosses.md new file mode 100644 index 0000000..ef5943d --- /dev/null +++ b/docs/de/guides/bulk-edit-glosses.md @@ -0,0 +1,79 @@ +# Anwendungsbeispiel: Massenbearbeitung von Glossaren + +Eine häufige Wartungsaufgabe: Die Schreibweise aller englischen Begriffserklärungen in einem Lexikon vereinheitlichen (britisch → amerikanisch oder umgekehrt), ohne dabei andere Teile der Datei zu verändern. Hier wird ein Skript Schritt für Schritt erläutert, das Daten lädt, bearbeitet, validiert und speichert – dabei wird gezeigt, wie die Bearbeitungs-API und die Genauigkeitsgarantie zusammenwirken. + +## Das Drehbuch + +```python +import sys + +import sil_lift + +path = "dictionary.lift" +lex = sil_lift.load(path) + + +def iter_senses(senses): + """Gibt jede Bedeutung zurück, einschließlich Unterbedeutungen (rekursiv).""" + for sense in senses: + yield sense + yield from iter_senses(sense.subsenses) + + +edited_glosses = 0 + +for entry in lex.entries: + for sense in iter_senses(entry.senses): + for gloss in sense.glosses: + if gloss.lang != "en": + continue + old = str(gloss.text) + new = old.replace("colour", "color") + if new != old: + gloss.text = sil_lift.Text([new]) + edited_glosses += 1 + +changed = lex.changed_entries() + +errors = [p for p in lex.iter_problems() if p.level == "error"] +if errors: + for problem in errors: + print(problem) + sys.exit(f"Abbruch: {len(errors)} Validierungsfehler, nichts gespeichert") + +lex.save() +print(f"Bearbeitete {edited_glosses} Glossare in {len(changed)} Einträgen") +``` + +Ein paar Dinge, die es zu beachten gilt: + +- `Sense.subsenses` ist selbst eine `list[Sense]`, daher wird sie von `iter_senses` rekursiv durchlaufen – eine Massenbearbeitung, die nur `entry.senses` durchläuft, würde alle unter einer Unterbedeutung verschachtelten Erläuterungen stillschweigend überspringen. +- `gloss.text` ist ein `Text` und keine einfache Zeichenkette: `str(gloss.text)` wandelt ihn für den Abgleich in eine Zeichenkette um, und die Ersetzung wird mit `sil_lift.Text([new])` zurückgeschrieben, anstatt die Zeichenkette direkt zu ändern. +- `lex.changed_entries()` gibt an, welche Einträge sich von der geladenen Datei unterscheiden. Da die Zusammenfassung eines Eintrags dessen gesamten Teilbaum abdeckt, wird bei einer Bearbeitung einer verschachtelten Teilbedeutung der Eintrag gemeldet, in dem diese enthalten ist. + - Da serialisierte Inhalte verglichen werden, wird die Zuweisung eines Werts zu einem Feld, den dieses bereits hatte, nicht gemeldet. + - Es werden ausschließlich inhaltliche Änderungen gemeldet; `lex.added_entries()` und `lex.removed_entries()` erfassen Einträge, die seit dem Laden hinzugekommen sind bzw. verschwunden sind. + - Es gibt die Einträge selbst zurück, unabhängig davon, ob `id` doppelt vorhanden ist oder fehlt (was LIFT zulässt). + - Als Zahl ist sie nur dann aussagekräftig, wenn es etwas gibt, mit dem man sie vergleichen kann. Wenn der Byte-Scanner das Lesen der Quelle verweigert – sei es aufgrund einer nicht ASCII-kompatiblen Kodierung oder einer Diskrepanz zwischen Scanner und Parser –, gibt es keine Basislinie, und `changed_entries()` meldet _jeden_ Eintrag. Das ist die ehrliche Antwort in Bezug auf einen Schreibschutz, da `save()` in diesem Fall die gesamte Datei erneut serialisiert; das bedeutet jedoch, dass der Wert der Größe des Lexikons entspricht und nicht der Größe der Bearbeitung. +- `lex.changes()` gibt an, ob sich das Dokument _überhaupt_ geändert hat. Es umfasst nicht nur die Einträge, sondern auch die Kopfzeile, das Stammelement und jedes `.lift-ranges`-Element. + - Es ist nur dann falsch, wenn `save()` die Quellbytes reproduzieren würde; daher ist `if not lex.changes(): ...` der richtige Weg, um einen unnötigen Schreibvorgang zu überspringen. Die Garantie gilt nur in eine Richtung: Bei einem Dokument, das neu geschrieben würde, wird niemals „nichts zu schreiben“ gemeldet, während eine Änderung, die eine vollständige Neuserialisierung erzwingt, wieder zu den ursprünglichen Bytes führen kann und dennoch gemeldet wird. + - Da dabei der Inhalt und nicht der Speicherort verglichen wird, sollten Sie damit nur das Speichern am aktuellen Speicherort absichern: `lex.save(some_other_dir / "dictionary.lift")` schreibt das Dokument und die zugehörigen Dateien an einen Speicherort, der noch leer ist – unabhängig davon, ob sich etwas geändert hat oder nicht. + - Es handelt sich um eine Sicherheitsmaßnahme, nicht um eine Beschleunigung – die Ausführung dieses Befehls wertet jeden Eintrag aus, was dem Aufwand entspricht, den `save()` betreibt, um zu entscheiden, welche Quellbytes wiederverwendet werden können. Was Sie also überspringen, ist der Schreibvorgang selbst (unveränderter Zeitpunkt der Dateiänderung, kein falscher Diff), nicht der Aufwand für die Entscheidung. +- Bei der Validierung im Arbeitsspeicher (`lex.iter_problems()`) wird der bearbeitete Zustand zunächst serialisiert, sodass er die Änderungen korrekt widerspiegelt, bevor Daten auf die Festplatte geschrieben werden. Ein Abbruch bei jedem `"error"`-Level-`Problem` – Warnungen werden dem Aufrufer zur Entscheidung überlassen – bedeutet, dass eine fehlerhafte Bearbeitung niemals die Funktion `save()` erreicht. + +Nicht nur Glanzlacke lassen sich auf diese Weise gut auftragen. Die gleiche `Multitext`-Zuordnungsfläche gilt für Definitionen und alle anderen mehrsprachigen Felder eines Eintrags oder einer Bedeutung: + +```python +sense.definition["en"] = "die Farbe eines Gegenstands" +``` + +## Ausführen + +Führen Sie eine Suche in einem kleinen Lexikon durch, in dem sowohl die Erläuterung als auch die Unterbedeutung mit „Farbe“ angegeben sind: + +``` +2 Begriffserklärungen in 1 Eintrag bearbeitet +``` + +## Der Lohn der Treue + +Die Garantie gilt pro _Eintrag_: Ein Eintrag, dessen Modell sich nicht geändert hat, wird **byte-identisch** so zurückgegeben, wie er eingelesen wurde, und nur die Einträge, die Sie tatsächlich bearbeitet haben, werden erneut serialisiert. In der obigen Ausführung wurden bei einem Eintrag die Glossen bearbeitet – alle anderen Einträge in der Datei behielten ihre exakten Bytes bei. (Beachten Sie die Detailgenauigkeit: Wenn Sie einen beliebigen Teil eines Eintrags bearbeiten, wird der gesamte Eintrag neu serialisiert, einschließlich der davon nicht betroffenen verwandten Bedeutungen.) Das Bearbeiten eines Eintrags in einem Lexikon mit 50.000 Einträgen führt daher zu einer Diff-Datei, die nur einen Eintrag betrifft, und nicht zu einer neu formatierten Datei. Den genauen Vertragsinhalt finden Sie unter [Fidelity-Garantien](../fidelity.md). diff --git a/docs/de/guides/cli.md b/docs/de/guides/cli.md new file mode 100644 index 0000000..f57f035 --- /dev/null +++ b/docs/de/guides/cli.md @@ -0,0 +1,70 @@ +# Die Befehlszeile + +Durch die Installation des Pakets (`pip install sil-lift`) wird auch der Befehl `sil-lift` installiert – ein unterstütztes Tool im Stil von LiftTools, das im Lieferumfang des Pakets enthalten ist (und für `validate` ein Beispiel für die Anwendung der Bibliotheks-API). + +``` +sil-lift validate PATH [--format {text,json}] [--strict] [--no-check-media] [--require-ids] + alle Probleme, mit Datei/Eintrag/Zeile; Beenden mit Status 1 bei Fehlern +sil-lift stats PATH [--format {text,json}] + Anzahl der Einträge/Bedeutungen/Sprachen (Streaming; beliebige Größe) +sil-lift sort PATH [-o OUT] kanonisch sortierte, diff-fähige Kopie (Standard: an Ort und Stelle) +sil-lift check-media PATH Bericht über fehlende und verwaiste Medien; Beendet mit 1, wenn Medien fehlen +sil-lift export PATH [-o OUT] [--langs L] [--tsv] + eine Zeile pro Blatt-Sense (Subsenses abgeflacht) in CSV/TSV (Streaming) +``` + +`--format json` gibt ein einzelnes JSON-Objekt an die Standardausgabe aus (und sonst nichts), das für CI- und Automatisierungszwecke verwendet werden kann; siehe das Schema im folgenden Beispiel. `--strict` behandelt Warnungen als Fehler und gibt den Wert 1 zurück, wenn welche gefunden werden – verwenden Sie diese Option, um einen Build davon abhängig zu machen, dass überhaupt keine Warnungen auftreten, anstatt nur von Fehlern. `--no-check-media` überspringt die Überprüfung des Vorhandenseins der Medien im Dateisystem (wodurch `missing-media`-Fehler unterdrückt werden), was bei der Validierung eines frisch generierten Exports nützlich ist, dessen Audio- und Fotodateien sich nicht im selben Ordner, sondern an einem anderen Speicherort befinden. `--require-ids` führt zudem zu einem Fehler (einem `missing-id`-Fehler), wenn bei einem Eintrag die `guid` fehlt oder bei einem Sense die `id` fehlt – dies ist strenger als bei LIFT und dient Workflows, bei denen der Reimport anhand einer stabilen ID erfolgt. Wird `-` als Pfad übergeben, wird das Dokument aus der Standard-Eingabe gelesen (ein über eine Pipe übermitteltes Dokument hat keinen Ordner, daher werden die zugehörige Datei `.lift-ranges` und die Medien nicht aufgelöst). `stats` akzeptiert ebenfalls die Option `--format json` und gibt die Zählwerte als einzelnes JSON-Objekt aus. + +!!! note + Die Exit-Codes von `validate` und das Schema von `--format json` stellen eine unterstützte Automatisierungsschnittstelle dar: Beide werden durch Tests abgedeckt und ändern sich nur gemäß SemVer. + +`sort` schreibt nur die `.lift`-Datei neu; die zugehörigen `.lift-ranges`-Dateien bleiben unberührt +(sortieren Sie diese separat mit der `RangesFile`-API). + +`validate`, `stats`, `check-media` und `export` akzeptieren ebenfalls ein komprimiertes LIFT-Paket (eine `.zip`-Datei in einem der beiden Layouts – entweder mit Dateien im Stammverzeichnis des Archivs oder verschachtelt unter einem Ordner der obersten Ebene); dieses wird in ein temporäres Verzeichnis entpackt und nach Abschluss des Befehls gelöscht. Die Streaming-Befehle `stats` und `export` extrahieren nur die `.lift`-Datei selbst, sodass sie bei datenintensiven Paketen ressourcenschonend bleiben; `validate` und `check-media` benötigen den gesamten Ordner und extrahieren dessen gesamten Inhalt. + +Beispiele: + +``` +$ sil-lift validate dictionary.lift +Fehler [dangling-ref] dictionary.lift:88 (Eintrag „apu“): Die Referenz „nope“ entspricht keiner Eintrags-ID/GUID oder Bedeutungs-ID +Warnung [uri-not-rfc] dictionary.lift:6: : Windows-Laufwerksbuchstabe als URI-Autorität verwendet (im FLEx-Stil file://C:/) +1 Fehler, 1 Warnung + +$ sil-lift validate dictionary.lift --format json +{ + „problems“: [ + { + "level": "error", + "code": "dangling-ref", + "message": "ref 'nope' passt zu keiner Eintrags-ID/GUID oder Sinn-ID", + "file": "dictionary.lift", + "entry_id": "apu", + "guid": null, + "line": 88 + }, + { + "level": "warning", + "code": "uri-not-rfc", + "message": ": Windows-Laufwerksbuchstabe als URI-Autorität verwendet (im FLEx-Stil file://C:/)", + "file": "dictionary.lift", + "entry_id": null, + "guid": null, + "line": 6 + } + ], + „summary“: { + „errors“: 1, + „warnings“: 1 + } +} + +$ sil-lift stats sango.lift +Einträge: 3507 +Bedeutungen: 4541 +... + +$ sil-lift export dictionary.lift --langs en,fr -o dictionary.csv +``` + +Exit-Codes: `0` erfolgreich (Warnungen zulässig, sofern nicht `--strict` angegeben), `1` Fehlermeldungen (Validierungsfehler / fehlende Medien / Warnungen bei Angabe von `--strict`), `2` unlesbare Eingabe. diff --git a/docs/de/guides/folder-media.md b/docs/de/guides/folder-media.md new file mode 100644 index 0000000..1de7af6 --- /dev/null +++ b/docs/de/guides/folder-media.md @@ -0,0 +1,41 @@ +# Der LIFT-Ordner: Sortimente und Medien + +Ein LIFT-Lexikon besteht in der Regel aus einem _Ordner_: der `.lift`-Datei, einer oder mehreren zugehörigen `.lift-ranges`-Dateien (Sidecar-Dateien) sowie den Mediendateien im Verzeichnis `audio/` bzw. `pictures/`. + +## Bereiche + +```python +lex = sil_lift.load("dictionary.lift") # Begleitdateien werden automatisch nachverfolgt + +lex.ranges_files # {Path(...): RangesFile} +lex.all_ranges() # zusammengeführte {id: Range}-Ansicht +lex.all_ranges()["grammatical-info"].elements +``` + +Die Companion-Erkennung berücksichtigt die reale Welt: Es wird ein `range/@href` verwendet, das auf eine vorhandene Datei verweist; Die losen absoluten `file://C:/...`-href-Angaben von FieldWorks greifen auf den Basisnamen des href neben der `.lift`-Datei zurück; und das herkömmliche Geschwisterelement `.lift-ranges` wird auch dann erkannt, wenn nichts darauf verweist. + +`lex.save()` schreibt die `.lift`-Datei und alle nachverfolgten Companion-Dateien gemeinsam. Änderungen an einer `RangesFile` werden wieder in _diese_ Datei gespeichert; unveränderte Bereiche behalten ihre genauen Byte-Werte bei. Einzelbetrieb: + +```python +ranges = sil_lift.RangesFile.load("dictionary.lift-ranges") +ranges.find("grammatical-info") +ranges.sort() +ranges.save() +``` + +Übergeben Sie `resolve_ranges=False` an `load()`, um die Erkennung von Begleitkomponenten zu überspringen. + +## Medien + +```python +for ref in lex.media_refs(): # alle und + print(ref.kind, ref.href, ref.entry_id) + +lex.missing_media() # Verweise, deren Dateien nicht vorhanden sind +``` + +Die Auflösung erfolgt nach dem üblichen Schema: Ein relativer href-Link wird unverändert geprüft (Backslashes werden normalisiert – WeSay schreibt „pictures\photo with space.png“) und unter „audio/“ (für Aussprachedateien) oder „pictures/“ (für Abbildungen) gesucht. Externe/absolute „hrefs“ können nicht überprüft werden und werden übersprungen. + +## Sonstige Ordnerinhalte + +Ein LIFT-Ordner enthält oft Dateien, die sil-lift nicht modelliert – beispielsweise das Schriftsystem LDML unter `WritingSystems/`, die Audio- und Bilddateien zur Einwilligung der Sprecher von „The Combine“ unter `consent/` und Ähnliches; `load()`/`save()` lassen diese unberührt, und [`Lexicon.save_zip()`](lift-export-interop.md) übernimmt sie beim Verpacken des Ordners unverändert. diff --git a/docs/de/guides/large-files.md b/docs/de/guides/large-files.md new file mode 100644 index 0000000..6d1c8f9 --- /dev/null +++ b/docs/de/guides/large-files.md @@ -0,0 +1,27 @@ +# Große Dateien (Streaming) + +`load()` baut den gesamten Objektgraphen auf. Bei Lexika mit einer Größe von mehreren hundert MB verarbeitet die Streaming-API jeweils einen Eintrag nach dem anderen in einem begrenzten Speicherbereich – dabei handelt es sich um denselben `Entry`-Typ, sodass Code, der für einen Modus geschrieben wurde, auch im anderen Modus funktioniert. + +```python +import sil_lift + +with sil_lift.open_reader("big.lift") as reader: + header = reader.header # wird vorab geparst (steht vor den Einträgen) + for entry in reader: # verzögerter Iterator[Entry] + ... +``` + +```python +mit sil_lift.open_reader("big.lift") als reader, sil_lift.open_writer( + "out.lift", header=reader.header, producer="my-script" +) als writer: + for entry in reader: + if not entry.date_deleted: # z. B. Tombstones entfernen + writer.write(entry) +``` + +Anmerkungen: + +- Die Ausgabe des Writers entspricht genau dem, was der kanonische Serializer für das gesamte Dokument bei denselben Inhalten erzeugen würde – die beiden Modi weichen nie voneinander ab. +- Im Streaming-Modus werden keine Quellbytes wiederverwendet: Die Ausgabe ist immer kanonisch. LIFT-Reste auf Root-Ebene – Kommentare zwischen Einträgen und schemenfremde Attribute unter `` – werden nicht übernommen; Einträge und der Header sind vollständig, einschließlich der Reste. +- Wenn im Hauptteil eines `open_writer`-Blocks ein Fehler ausgelöst wird, bleibt die Datei sichtbar unvollständig (kein abschließendes ``) – ein nur zur Hälfte geschriebenes Lexikon darf nicht vollständig erscheinen. diff --git a/docs/de/guides/lift-export-interop.md b/docs/de/guides/lift-export-interop.md new file mode 100644 index 0000000..cbde2c7 --- /dev/null +++ b/docs/de/guides/lift-export-interop.md @@ -0,0 +1,112 @@ +# Erstellung eines konformen LIFT + +Dieser Leitfaden richtet sich an alle, die einen LIFT-_Exporter_ schreiben – also Code in einer beliebigen Sprache, der das Datenmodell einer anderen Anwendung in LIFT 0.13 umwandelt. `sil-lift` erfüllt bei dieser Arbeit zwei Funktionen: zum einen dient es als Konformitätsprüfung, die die Ausgabe anhand des Schemas und der Semantik, die ein Schema nicht ausdrücken kann, überprüft, und zum anderen als Referenz für die Formen und Textregeln, denen die Ausgabe entsprechen muss. + +Das Schreiben von LIFT ist viel einfacher als dessen Analyse: Ein Exporter gibt nur die Teilmenge der Konstrukte aus, die sein eigenes Modell erzeugt, und muss sich nie mit den optionalen Elementen der vollständigen Spezifikation auseinandersetzen. Das Schwierige sind die Details – der `.lift-ranges`-Begleiter, der schriftsystemspezifische Text, die stabilen IDs und die XML-Escape-Zeichen – und genau diese werden durch die folgenden Prüfungen erfasst. + +## ZIP-Dateien + +LIFT wird in der Regel als einzelne `.zip`-Datei weitergegeben – sowohl FieldWorks als auch The Combine importieren und exportieren auf diese Weise –, daher liest und schreibt `sil-lift` komprimierte Pakete direkt, und zwar in beiden vom Ökosystem verwendeten Strukturen: entweder mit den Dateien im Stammverzeichnis des Archivs oder verschachtelt unter einem Ordner der obersten Ebene. + +- **Hinweis:** `sil_lift.load("package.zip")` entpackt die Datei in ein temporäres Verzeichnis, sucht die einzelne `.lift`-Datei und lädt sie (Begleitdateien und Medien werden wie gewohnt aufgelöst). + - Die CLI-Befehle `validate`, `stats`, `check-media` und `export` akzeptieren ebenfalls einen `.zip`-Pfad, sodass das unten stehende Skript direkt auf ein Paket in seiner aktuellen Form angewendet werden kann. + - die Streams `stats` und `export` und extrahieren Sie nur die `.lift`-Datei statt des gesamten Pakets – so bleibt der Aufwand bei medienintensiven Paketen gering, und die Extraktionsbeschränkung gilt nur für die `.lift`-Datei und nicht für alles andere daneben. + - Die Extraktion ist auf 10 GiB und 100.000 Elemente begrenzt; ein Paket, das eine dieser Grenzen überschreitet, wird mit einem `LiftParseError` abgelehnt, ebenso wie eines, dessen Elementpfade über das Extraktionsverzeichnis hinausgehen. +- **Schreiben Sie:** `Lexicon.save_zip("out.zip", wrap_folder="MyDict")` packt die `.lift`-Datei, deren `.lift-ranges` sowie alle anderen Dateien im Quellordner (media, `WritingSystems/`, `consent/`, ...) zusammen. in eine ZIP-Datei. + - `wrap_folder` ist standardmäßig auf einen Ordner auf oberster Ebene eingestellt, der nach der ZIP-Datei benannt ist (gemäß der Importkonvention von FieldWorks/Combine); übergeben Sie `False`, um ein flaches Archiv zu erhalten. + +Die Dateien `.lift` und `.lift-ranges` behalten ihre Byte-Genauigkeit innerhalb des Pakets bei; der ZIP-Container selbst ist nicht byte-reproduzierbar. + +## Die Ausgabe als Konformitätsprüfung validieren + +Richten Sie den Befehl `sil-lift validate` auf die erstellte `.lift`-Datei aus. Es führt RELAX NG-Prüfungen durch (sowohl für die `.lift`-Datei als auch für die zugehörige `.lift-ranges`-Datei) sowie semantische Prüfungen, die die Grammatik nicht ausdrücken kann: frei schwebende `relation`-/`variant`-Verweise, doppelte GUIDs, die Integrität der übergeordneten Elemente von Bereichselementen, in ihrem Bereich nicht definierte Werte für Merkmale und grammatikalische Informationen sowie `range/@href`-Verweise in Headern, die auf kein zugehöriges Element verweisen. + +Bei CI: Bei jedem Fehler den Vorgang abbrechen und maschinenlesbare Ergebnisse ausgeben: + +``` +sil-lift validate export.lift --strict --no-check-media --format json +``` + +- Mit `--strict` führen bereits Warnungen (nicht nur Fehler) zum Abbruch der Ausführung. +- `--no-check-media` überspringt die Überprüfung auf das Vorhandensein von Mediendateien im Dateisystem, deren `missing-media`-Funde irreführend sind, wenn sich die Audio- und Fotodateien in der CI nicht im selben Ordner wie die `.lift`-Datei befinden. +- `--format json` gibt anstelle von lesbarem Text ein einzelnes JSON-Objekt aus (`{"problems": [...], "summary": {...}}`); die Rückgabecodes und das Schema bilden eine unterstützte, SemVer-konforme Schnittstelle (siehe [das Handbuch zur Befehlszeile](cli.md)). +- `--require-ids` gibt zusätzlich Fehlermeldungen aus, wenn bei Einträgen eine `guid` fehlt oder bei Sensoren eine `id` fehlt – nützlich, wenn bei einem späteren erneuten Import die Einträge aktualisiert und nicht dupliziert werden sollen. + +Schützen Sie sich vor unbemerktem Datenverlust (dem Fehlermodus, der den Export als flache CSV-Datei verlustbehaftet macht), indem Sie die Zählwerte mit `stats --format json` für Ihr Quellmodell überprüfen: + +``` +sil-lift stats export.lift --format json +``` + +Es gibt die Anzahl der „Einträge“, „Bedeutungen“, „Beispiele“, „Medienverweise“, „Sprachen“ sowie die Anzahl der „Merkmale“ pro Name an. + +### Ausführung von „gate“ ohne Python-Toolchain + +Die CI eines TypeScript- oder C#-Projekts kann dieselbe Überprüfung über die mitgelieferte GitHub-Action ausführen, ohne dass Python installiert werden muss: + +```yaml +- uses: sillsdev/python-sil-lift@v0.1.0 + with: + path: export.lift + strict: "true" + no-check-media: "true" + format: json +``` + +oder das Container-Image, das anhand der `Dockerfile`-Datei aus dem Repository erstellt wurde: + +``` +docker build -t sil-lift . +docker run --rm -v "$PWD:/work" -w /work sil-lift validate export.lift --strict +``` + +## Der `.lift-ranges`-Begleiter + +Kontrollierte Vokabulare – Wortarten, semantische Domänen und alle anderen auf Merkmalen basierenden Wertemengen – befinden sich in einer gleichrangigen `.lift-ranges`-Datei, auf die in der Datei `
` verwiesen wird: + +```xml +
+ + + + +
+``` + +Das Begleitheft enthält die vollständige Definition der einzelnen Bereiche. Die Werte sind ``s; `parent` bildet eine Hierarchie; `label` / `abbrev` / `description` sind Multitexte: + +```xml + + + + + +
n
+
+
+ + + + + +
+``` + +Ein Eintrag verweist dann anhand der ID auf einen Wert: Die Wortart einer Bedeutung lautet ``, und eine semantische Domäne lautet ``. `sil-lift validate` gibt eine Warnung (`undefined-range-value`) aus, wenn ein Wert nicht innerhalb seines Bereichs definiert ist, und einen Fehler (`range-parent`), wenn ein `parent` keine Geschwister-ID ist – geben Sie daher die Bereiche an, die Ihre Daten tatsächlich verwenden. Siehe auch [Bereiche und Medien](folder-media.md). + +Wenn Sie den Export in Python erstellen, erstellen `Lexicon.add_ranges_file()`, `RangesFile.add_range()` und `Range.add_element()` das zugehörige Objekt und fügen die Header-Referenzen für Sie hinzu; `open_writer(..., ranges=...)` führt dasselbe auf dem Streaming-Pfad durch. + +## Text und Mehrfachtext + +Jede Zeichenkette in einer menschlichen Sprache in LIFT ist ein _Multitext_: ein `
` pro Schriftsystem, wobei jedes ein `` umschließt: + +```xml + + Kanga +
Huhn
+
+``` + +Ein Modell, das Zeichenfolgen anhand des Sprachcodes indiziert (ein `MultiString`, ein `Record`, ein `dict[str, str]`), lässt sich eins-zu-eins auf dieses Modell abbilden: Ein Eintrag pro Schlüssel entspricht einem `
`. In einem einzelnen Multitext ist höchstens eine Form pro Sprache zulässig – andernfalls gibt `sil-lift` die Warnung `duplicate-form-lang` aus. + +Das XML-Escaping ist der einzige Teil, bei dem es wirklich auf Korrektheit ankommt. Im Elementtext müssen `&`, `<`, and `>` mit Escape-Zeichen versehen werden (`&`, `<`, `>`); in Attributwerten gilt dies auch für das Anführungszeichen. Der Autor von `sil-lift` wendet genau diese Regeln an und verändert niemals die Leerzeichen innerhalb von `` – er fügt dort keine Einrückungen hinzu, da dies die lexikalischen Daten verfälschen würde. Wenn Sie die Ausgabe des Serialisierers nachbilden möchten, sollten Sie die Escaping-Zeichen eines echten XML-Serialisierers verwenden (und nicht eine selbst programmierte Ersetzungsfunktion, bei der das `&`-Zeichen vergessen wird) und den Inhalt von `` Byte für Byte so belassen, wie er in Ihrer Quelle vorliegt. diff --git a/docs/de/guides/read-edit-write.md b/docs/de/guides/read-edit-write.md new file mode 100644 index 0000000..c16977d --- /dev/null +++ b/docs/de/guides/read-edit-write.md @@ -0,0 +1,66 @@ +# Lesen, bearbeiten, schreiben + +## Wird geladen + +```python +import sil_lift + +lex = sil_lift.load("dictionary.lift") +``` + +`load()` akzeptiert jedes wohlgeformte LIFT-**0.13**-Dokument – einschließlich schemaverstossender Dateien aus der Praxis. Alles, was das Modell nicht definiert (unbekannte Elemente/Attribute, Kommentare), wird verlustfrei als LIFT-Rest im undurchsichtigen `extra`-Feld jedes Knotens übertragen. Andere LIFT-Versionen lösen einen `LiftParseError` aus, in dem die Version angegeben wird. + +## Das Modell + +Jedes LIFT-Element ist eine typisierte Datenklasse: `Entry`, `Sense`, `Example`, `Pronunciation`, `Variant`, `Relation`, `Etymology`, `Reversal` und so weiter. Mehrsprachiger Text ist ein `Multitext`, der sich wie eine Zuordnung vom Sprachcode zu `Text` verhält: + +```python +entry = lex.find(id="abat") + +str(entry.lexical_unit["seh"]) # "abat" +entry.lexical_unit["en"] = "grove" # einfache Zeichenketten werden umgewandelt +"en" in entry.citation # False +``` + +`Text` ist strukturiert – eine geordnete Liste aus `str`- und `Span`-Fragmenten –, da `` verschachtelte ``-Markups enthalten kann. `str(text)` wandelt den Text in reinen Text um; die Fragmente behalten das Markup für den Hin- und Rücktransport bei. + +Glossare sind in LIFT _form-förmig_ (jedes `` enthält eine eigene Sprache), daher hat ein Sense `glosses: list[Form]` sowie eine Hilfsfunktion: + +```python +sense = entry.senses[0] +sense.gloss("en") # Text | None +entry.gloss_langs() # {"en", "id"} +``` + +## Speichern + +```python +lex.save() # zurück an den Ort, von dem es geladen wurde +lex.save("elsewhere.lift") +``` + +Einträge, die Sie nicht geändert haben, werden **byte-identisch** zurückgeschrieben; ein Dokument, das Sie überhaupt nicht geändert haben, ist vom ersten bis zum letzten Byte byte-identisch. Den genauen Vertragsinhalt finden Sie unter [Fidelity-Garantien](../fidelity.md). + +## Von Grund auf neu aufbauen + +```python +lex = sil_lift.Lexicon(producer="my-script 1.0") +entry = sil_lift.Entry(id="hello", guid="...") +entry.lexical_unit["en"] = "hello" +sense = sil_lift.Sense() +sense.glosses.append(sil_lift.Form("fr", sil_lift.Text(["bonjour"]))) +entry.senses.append(sense) +lex.entries.append(entry) +lex.save("new.lift") +``` + +## Kanonische Sortierung + +```python +lex.sort() # Einträge nach (GUID, ID); Bereiche/Felddefinitionen nach ID/Tag +lex.save() # Unveränderte Einträge behalten ihre exakten Bytes in der neuen Reihenfolge + +sil_lift.canonicalize("in.lift", "out.lift") # vollständig neu serialisiert, bereit für den Vergleich +``` + +Siehe auch: [Beispiel: Massenbearbeitung von Glossaren](bulk-edit-glosses.md). diff --git a/docs/de/guides/validate.md b/docs/de/guides/validate.md new file mode 100644 index 0000000..3e7ab1d --- /dev/null +++ b/docs/de/guides/validate.md @@ -0,0 +1,36 @@ +# Überprüfen + +Die Validierung erfolgt immer explizit – beim Laden und Speichern findet niemals eine implizite Validierung statt. + +```python +import sil_lift + +# Vollständig: ein „lazy“ Stream von Problemen (Schema + semantische Ebenen). +for problem in sil_lift.iter_problems("dictionary.lift"): + print(problem) + # Fehler [dangling-ref] dictionary.lift:88 (Eintrag apu): ref 'nope' passt zu ... + +# Fail-fast: Löst bei dem ersten Problem auf Fehler-Ebene einen LiftValidationError aus. +sil_lift.validate_file("dictionary.lift") + +# In-Memory-Zustand (wird zunächst serialisiert – ein dokumentierter Mehraufwand bei großen Lexika): +lex = sil_lift.load("dictionary.lift") +problems = list(lex.iter_problems()) +``` + +Jedes `Problem` enthält einen `Level` (`"error"`/`"warning"`), einen festen `Code`, eine `Meldung` sowie folgende Angaben: `Datei`, `Eintrags-ID`, `GUID`, `Zeile`. + +## Die Schichten + +1. **RELAX NG** gemäß der LIFT 0.13-Grammatik (aus „lift-standard“ übernommen – eine byteweise identische Kopie, die in dieses Paket integriert wurde). +2. **Ranges-Schema** – in diesem Projekt `lift-ranges-0.13.rng` – für jeden erfassten `.lift-ranges`-Begleiter. +3. **Semantische Prüfungen**, die die Grammatik nicht ausdrücken kann: `duplicate-guid`, `dangling-ref`, `range-parent`, `undefined-range-value`, `duplicate-form-lang`, `missing-media`. + +## Praktische FieldWorks (FLEx)-Ergebnisse + +FieldWorks schreibt systematisch bestimmte Inhalte, die von strengen Validierungstools abgelehnt werden. Hier sind die Richtlinien von sil-lift, damit echte Lexika sinnvoll validiert werden können: + +- `file://C:/...`-href-Attribute (ungültige URIs) werden als **Warnungen** (`uri-not-rfc`) gemeldet, nicht als Schemafehler – der C#-Validator hat sie nie abgelehnt. +- Rechtmäßig verschachtelte Kinder (z. B. in gewisser Weise `field, note, field, note`) werden **nicht** markiert, wodurch ein Fehlalarm in libxml2 umgangen wird. +- Bereichswerte werden unter der Unicode-NFC-Normalisierung verglichen – FLEx schreibt die `.lift`-Daten in NFC, die `.lift-ranges`-Daten jedoch in NFD innerhalb desselben Exports. +- Die `trait`/`field`-Erweiterungen von FLEx innerhalb von `range-element` **werden** gemeldet (Schemafehler in Bezug auf das Ranges-Schema): Es handelt sich dabei um echte Abweichungen von der Spezifikation. diff --git a/docs/de/index.md b/docs/de/index.md new file mode 100644 index 0000000..c6ec627 --- /dev/null +++ b/docs/de/index.md @@ -0,0 +1,32 @@ +# sil-lift + +Eine Python-Bibliothek für [LIFT](https://github.com/sillsdev/lift-standard) (Lexicon Interchange FormaT) 0.13: verlustfreies Lesen und Schreiben des LIFT-Ordners (`.lift` + `.lift-ranges` + Medienverweise), Schema- und semantische Validierung sowie kanonische Sortierung – mit Streaming-APIs für große Lexika. + +**Status: Vorabversion, befindet sich in aktiver Entwicklung.** + +## Installieren + +Aus [PyPI](https://pypi.org/project/sil-lift/): + +``` +pip install sil-lift # Bibliothek + der Befehl „sil-lift“ +``` + +Erfordert Python 3.11 oder höher. Die einzige Laufzeitabhängigkeit ist lxml. + +## Die 30-Sekunden-Tour + +```python +import sil_lift + +lex = sil_lift.load("thesaurus.lift") # erfasst auch Begleitbegriffe aus .lift-Bereichen + +for entry in lex.entries: + if "en" not in entry.gloss_langs(): + print(entry.id, str(entry.lexical_unit.get("seh") or "")) + +entry = lex.find(guid="0f5a9c3e-...") # oder lex.find(id="hoofd_a1b2") +entry.senses[0].definition["en"] = "head (anatomy)" + +lex.save() # Unveränderte Einträge sind byteweise identisch; bearbeitete Einträge werden neu serialisiert +``` diff --git a/docs/de/reference.md b/docs/de/reference.md new file mode 100644 index 0000000..c82ad48 --- /dev/null +++ b/docs/de/reference.md @@ -0,0 +1,9 @@ +# API-Referenz + +Die öffentliche API entspricht genau dem, was `sil_lift` auf der obersten Ebene erneut exportiert; Module mit einem Unterstrich als Präfix sind Implementierungsdetails. + +::: sil_lift +options: +show_root_heading: false +show_source: false +members_order: source diff --git a/docs/es/csharp-differences.md b/docs/es/csharp-differences.md new file mode 100644 index 0000000..74b41da --- /dev/null +++ b/docs/es/csharp-differences.md @@ -0,0 +1,52 @@ +# Diferencias con respecto a las bibliotecas de C\# + +sil-lift es, a grandes rasgos, análogo a las herramientas LIFT de SIL para C# —principalmente `SIL.Lift` en [libpalaso](https://github.com/sillsdev/libpalaso) (analizador sintáctico, validador, migrador, `LiftSorter`), `SIL.DictionaryServices` del mismo repositorio (el modelo `LexEntry`/`LexSense`, con su propio lector/escritor LIFT, que utilizan The Combine y WeSay), y los controladores LIFT de [Chorus](https://github.com/sillsdev/chorus). Se trata de una implementación nueva, no de una adaptación. En esta página se resumen los aspectos en los que el comportamiento difiere deliberadamente. + +## Ámbito de aplicación + +| Capacidad | Bibliotecas de C# | sil-lift | +| ----------------------------------- | ------------------------------------------------------- | ---------------------------------------------------------------------------- | +| Versiones de LIFT | 0,10–0,13 (migración incluida) | **Solo 0,13**; las versiones anteriores se rechazan con un error claro | +| Migración de versiones | `Migrator` (cadena XSLT) | ninguna — utiliza los XSLT de «lift-standard» para actualizaciones puntuales | +| Fusión/sincronización a tres bandas | Estribillo | fuera del ámbito de aplicación | +| Validación | Solo RELAX NG (`Validator`) | RELAX NG + comprobaciones de esquema y semánticas | +| Streaming | análisis sintáctico interno con granularidad de entrada | API pública `open_reader` / `open_writer` | + +## Formato de la API + +El analizador de `SIL.Lift` funciona mediante callbacks (`ILexiconMerger`): envía eventos de análisis a un consumidor. En cambio, sil-lift devuelve un grafo de objetos simple —clases de datos tipadas para cada elemento LIFT—, ya que los programadores de Python quieren objetos, no funciones de devolución de llamada. `SIL.DictionaryServices` superpone un modelo de objetos `LexEntry`/`LexSense` sobre `SIL.Lift`, pero, como modelo de aplicación, solo representa las construcciones que utilizan esas aplicaciones; por lo tanto, la reserialización a través de él no puede conservar el contenido ajeno al modelo de la misma forma que lo hacen el manejo de residuos LIFT y la fidelidad de bytes de sil-lift (véase más abajo). La API de streaming devuelve el _mismo_ tipo `Entry`, por lo que no hay que aprender un segundo modelo simplificado. + +## Fidelidad de ida y vuelta + +La diferencia más marcada y deliberada. Al guardar con `SIL.Lift`, se vuelve a serializar todo el documento. sil-lift garantiza: + +- un documento que no ha sufrido modificaciones se guarda **con los mismos bytes**, y +- Las entradas que no se modifican conservan sus bytes de origen exactos incluso cuando cambian otras entradas: se trata del mismo sistema de fragmentación en bloques de bytes por entrada que utiliza Chorus, que se aplica automáticamente. + +Consulta [las garantías de Fidelity](fidelity.md). + +## Validación + +El `Validator` de C# ejecuta una pasada de RELAX NG y devuelve los primeros errores en forma de cadenas de caracteres. sil-lift genera un flujo estructurado de «Problemas», cada uno de los cuales incluye el archivo, la entrada y la línea a los que se refiere, y su capa de esquema presenta tres divergencias deliberadas: + +- **Los URI no válidos son advertencias, no errores.** El motor RELAX NG de C# nunca ha aplicado el tipo de datos `anyURI`, por lo que FieldWorks (FLEx) lleva años incluyendo enlaces `file://C:/...` en léxicos reales. Si se rechazaran esos archivos, se marcarían prácticamente todas las exportaciones de FLEx. +- **Se aplican las reglas de Schematron** (como comprobaciones semánticas): tanto la validación en C# como la validación directa con lxml ignoraban silenciosamente los lenguajes de formulario duplicados y las coconstricciones similares en la gramática LIFT. +- **Las comparaciones entre archivos están normalizadas según Unicode**, ya que FLEx guarda el archivo `.lift` en NFC y el archivo complementario `.lift-ranges` en NFD. + +sil-lift también valida los archivos complementarios `.lift-ranges` de un léxico cargado comparándolos con un esquema para documentos de rangos independientes (proporcionado por `lift-standard` junto con la gramática LIFT básica) — cada archivo de rangos externo del que se realiza un seguimiento se comprueba cada vez que se valida el `.lift` — sin que exista dicho esquema (ni dicha comprobación) en el entorno de C#. (No existe ningún punto de entrada para validar un archivo `.lift-ranges` por sí solo, sin estar vinculado a un archivo `.lift`.) + +## Ordenación canónica + +`Lexicon.sort()` refleja las reglas básicas de `LiftSorter` (las entradas se ordenan por GUID sin distinguir entre mayúsculas y minúsculas; los rangos y los elementos de los rangos, por ID; las definiciones de los campos de encabezado, por etiqueta; los significados se mantienen en el orden del archivo; los espacios en blanco dentro de `` nunca se modifican), con tres diferencias: + +- Las entradas sin un GUID se ordenan de forma determinista por ID (`LiftSorter` da por hecho que existe un GUID); +- el orden es independiente de la configuración regional (puntos de código con mayúsculas y minúsculas ignoradas, no la clasificación de «cultura invariante» de .NET); +- Las listas del mismo tipo, como notas, relaciones y formularios, mantienen el orden del documento en lugar de volver a ordenarse por clave; la agrupación ya es determinista, y reordenarlas solo añade ruido a las diferencias. + +El archivo `canonicalizeLift.xsl` del repositorio de especificaciones no se utiliza en absoluto: elimina los espacios en blanco dentro del texto léxico (de forma destructiva) y los identificadores que genera varían en cada ejecución. + +## No se ha trasladado + +- Funcionalidades específicas de WeSay (panel de control y gestión de la configuración relacionada con los archivos LIFT). +- `SynchronicMerger` (fusión de actualizaciones de Chorus): la idea de la segmentación en bloques de bytes se mantiene en la capa de fidelidad, pero la fusión no. +- Análisis sintáctico del sistema de escritura LDML: los archivos de la carpeta `WritingSystems/` se tratan como contenido opaco de la carpeta. diff --git a/docs/es/fidelity.md b/docs/es/fidelity.md new file mode 100644 index 0000000..d7e7874 --- /dev/null +++ b/docs/es/fidelity.md @@ -0,0 +1,33 @@ +# Garantías de Fidelity + +LIFT es un formato de _intercambio_, por lo que la primera regla es **no descartar nunca lo que no entiendas**. El contrato de `sil-lift`, verificado por el conjunto de pruebas en cada ejecución (archivos del corpus más generación basada en propiedades): + +## Lectura + +Cualquier documento LIFT 0.13 bien formado se carga, incluso si el contenido no cumple con el esquema. Todo aquello que el modelo no defina se traslada al contenedor opaco `Extras` del nodo más cercano como _residuo LIFT_ —nombre que utiliza FieldWorks para referirse a este mismo concepto, y que almacena en un campo `LiftResidue`—: atributos y elementos desconocidos, comentarios XML e instrucciones de procesamiento, texto extrañado y atributos tipificados mal formados (una fecha incorrecta se mantiene como la cadena original en `Extras`; el campo tipado es `None`). + +## Guardar un documento sin modificaciones + +`load()` → `save()` sin modificaciones genera una **salida idéntica a nivel de bytes**: sin reformateo, sin reescapado, sin reordenación, incluyendo las marcas de orden de bytes y las declaraciones XML. Actualmente no hay ninguna lista de normalización: la identidad es exacta. + +Excepciones (el escritor recurre a la serialización canónica completa, que es semánticamente completa pero no conserva los bytes): + +- la codificación de origen no es compatible con ASCII (no es UTF-8/US-ASCII), o +- el código fuente contiene un DOCTYPE, o +- el escáner de bytes y el analizador sintáctico no coinciden en cuanto a la estructura de nivel superior del documento —por ejemplo, un segundo `
` que no cumple con las especificaciones, que el analizador sintáctico conserva solo una vez (el escáner es deliberadamente conservador: ante cualquier duda, no captura ningún byte del código fuente)—, o +- El código fuente se compiló en memoria, en lugar de cargarse desde un archivo. + +## Guardar un documento editado + +- **Las entradas no modificadas se emiten tal cual, a partir de sus bytes originales.** Una entrada se considera modificada si alguna parte de su objeto de modelo ha cambiado desde el análisis (lo cual se detecta mediante una instantánea de serialización canónica, no mediante un indicador de cambios). +- **Las entradas modificadas se vuelven a serializar de forma canónica y completa**: UTF-8, sangría de 2 espacios _fuera_ del contenido mixto (los espacios en blanco dentro de `` y `` nunca se modifican), una agrupación de elementos secundarios documentada por cada elemento (p. ej., entrada: unidad léxica, cita, pronunciaciones, variantes, acepciones, notas, relaciones, etimologías, anotaciones, rasgos, campos), orden fijo de los atributos, fechas en formato ISO-8601 (`Z` para UTC). Todos los residuos se vuelven a emitir; su posición se restablece en el índice secundario original, vinculada a la nueva lista secundaria (se trata de una aproximación: las posiciones exactas en bytes solo se garantizan para las entradas que no se han modificado). +- Al añadir, eliminar o reordenar entradas, se vuelve a serializar la estructura del documento, pero se siguen emitiendo tal cual los bytes de cada entrada que no haya sufrido cambios. + +!!! note ""El XML canónico" que aparece aquí no está relacionado con ningún otro XML canónico." + En esta página, por «forma canónica» se entiende la forma documentada propia de «sil-lift», descrita en uno de los puntos anteriores. No guarda relación alguna con el proceso «Canonical XML (C14N)» del W3C. No tiene nada que ver con la clase `CanonicalXmlSettings` de `SIL.Core`. + +## Aproximaciones conocidas (solo nodos tocados) + +- Los comentarios que se encuentran _dentro_ de una ejecución de `` se conservan, pero se trasladan junto a la ejecución, en lugar de mantenerse en su posición exacta en caracteres. +- El orden de subelementos de tipo «cross» dentro de un elemento editado se normaliza según la agrupación canónica (la propiedad `interleave` del esquema LIFT hace que este orden carezca de importancia semántica). +- Un elemento multitexto que está presente pero que no contiene nada —ni formas, ni residuos—, por ejemplo, ``, no se vuelve a emitir. El modelo representa estos campos como un `Multitext` siempre presente (`lexical-unit`, `citation`, `definition`, el `usage` de una relación y `label` / `abreviatura` / `descripción` en referencias URL, rangos, elementos de rango y el encabezado), por lo que uno vacío es indistinguible de uno ausente tras el análisis sintáctico. No se pierde nada a nivel semántico. diff --git a/docs/es/guides/build-export.md b/docs/es/guides/build-export.md new file mode 100644 index 0000000..0359e41 --- /dev/null +++ b/docs/es/guides/build-export.md @@ -0,0 +1,168 @@ +# Ejemplo práctico: cómo crear una exportación LIFT desde cero + +Si estás exportando los datos de otra aplicación en formato LIFT —la tarea que subyace a [La generación de LIFT conforme a las normas](lift-export-interop.md)—, `sil-lift` puede construir el documento objeto a objeto y serializarlo, en lugar de generar el XML manualmente. Aquí se explica paso a paso cómo funciona un script que crea una entrada con los elementos que contiene un diccionario real (varios sistemas de escritura, una pronunciación, un significado con un ejemplo, una ilustración, una característica de dominio semántico y un campo específico de la aplicación), escribe los vocabularios controlados en un archivo complementario `.lift-ranges`, los valida y los guarda. + +## El guión + +```python +from pathlib import Path + +import sil_lift + +lex = sil_lift.Lexicon(producer="my-exporter") + +# Una entrada, creada a partir del modelo de origen. +entry = sil_lift.Entry(id="kanga", guid="6b9e7c2a-3f4d-4a1b-8c5e-2d9f0a1b2c3d") +entry.lexical_unit["seh"] = "nkhuku" +entry.lexical_unit["pt"] = "galinha" + +pron = sil_lift.Pronunciation() +pron.forms["en"] = "Hablante: Ana" # La convención de etiquetas de hablantes de The Combine +pron.media.append(sil_lift.URLRef(href="audio/nkhuku.wav")) +entry.pronunciations.append(pron) + +sense = sil_lift.Sense(id="kanga_s1") +sense.grammatical_info = sil_lift.GrammaticalInfo(value="Noun") +sense.glosses.append(sil_lift.Form(lang="en", text=sil_lift.Text(["chicken"]))) +sense.definition["en"] = "ave doméstica criada por sus huevos y su carne" + +example = sil_lift.Example() +example.forms["seh"] = "Ndinafuna nkhuku." +translation = sil_lift.Translation() +translation.forms["en"] = "I want a chicken." +example.translations.append(translation) +sense.examples.append(example) + +photo = sil_lift.URLRef(href="pictures/hen.jpg") +photo.label["en"] = "Una gallina" +sense.illustrations.append(photo) + +sense.traits.append(sil_lift.Trait(name="semantic-domain-ddp4", value="1.6.1.2")) + +scientific = sil_lift.Field(type="scientific-name") # un campo adicional específico de la aplicación +scientific.content["en"] = "Gallus gallus domesticus" +sense.fields.append(scientific) + +entry.senses.append(sense) +lex.entries.append(entry) + +# Los vocabularios controlados a los que hace referencia la entrada, en un archivo .lift-ranges complementario. +ranges = sil_lift.RangesFile() +ranges.add_range("grammatical-info").add_element("Noun").label["en"] = "noun" +ranges.add_range("semantic-domain-ddp4").add_element("1.6.1.2").label["en"] = "Bird" +lex.add_ranges_file(ranges, href="birds.lift-ranges") + +# Valida lo que escribiría save(), antes de guardar en el disco. +problems = list(lex.iter_problems()) +print(f"validación: {len(problems)} problema(s)") + +out = Path("export") +out.mkdir(exist_ok=True) +lex.save(out / "birds.lift") +print("=== birds.lift ===") +print((out / "birds.lift").read_text(encoding="utf-8"), end="") +print("=== birds.lift-ranges ===") +print((out / "birds.lift-ranges").read_text(encoding="utf-8"), end="") +``` + +## Qué produce + +`validación: 0 problema(s)`, y a continuación el `.lift` y su equivalente, uno al lado del otro: + +``` +=== birds.lift === + + +
+ + + + +
+ + + + nkhuku + +
+ galinha +
+
+ +
+ Speaker: Ana +
+ +
+ + + + chicken + + +
+ a domestic fowl kept for its eggs and meat +
+
+ +
+ Ndinafuna nkhuku. +
+ +
+ I want a chicken. +
+
+
+ + + + + +
+ Gallus gallus domesticus +
+
+
+
+
+=== birds.lift-ranges === + + + + + + + + + + + + + +``` + +## Notas sobre la API + +- Campos multitexto (`lexical_unit`, `definition`, una etiqueta de `Form`/`URLRef`, el contenido de un `Field`, etc.) Selecciona una cadena por sistema de escritura a través de la interfaz de mapeo: `entry.lexical_unit["seh"] = "nkhuku"` añade un `
`. Un modelo de origen que indexa las cadenas por código de idioma se corresponde directamente con esto. +- `RangesFile.add_range()` / `Range.add_element()` crean los vocabularios controlados, y `Lexicon.add_ranges_file(ranges, href=...)` adjunta el archivo complementario y añade las referencias de encabezado ``, de modo que las entradas `` y `` se resuelven en función de los rangos que hayas definido. +- Un `URLRef` es un atributo `href` junto con un texto múltiple opcional (leyenda o etiqueta), que se utiliza tanto para `` (audio) como para `` (fotos). La pronunciación aquí sigue la convención de The Combine, que consiste en una forma «en» que se lee « ». +- Datos específicos de la aplicación que no contengan desplazamientos de ida a casa nativos de LIFT como « » (o « »): FieldWorks los interpreta como campos personalizados y The Combine los conserva. +- Asigna a cada entrada un `guid` real y estable (por ejemplo, generado con `uuid.uuid4()`, que se reutilice en todas las exportaciones): así, si se vuelve a importar más adelante, la entrada se actualizará in situ en lugar de duplicarse. El comando `sil-lift validate --require-ids` garantiza el cumplimiento de esta norma. +- `lex.iter_problems()` comprueba la validez del documento almacenado en memoria (lo que escribiría `save()`) antes de que nada se guarde en el disco; aquí está en perfecto estado. Dado que el léxico aún no tiene ninguna carpeta, se omiten las comprobaciones de «media-presence» y «companion-href»: ejecuta [`sil-lift validate`](cli.md) sobre el resultado guardado (o con `--no-check-media`) una vez que los archivos de audio y las fotos estén en su sitio. + +## Embalaje + +`lex.save("export/birds.lift")` guarda la estructura de la carpeta (los archivos `.lift` y `.lift-ranges` uno al lado del otro). Para generar un único paquete comprimido que FieldWorks y The Combine puedan importar directamente, utiliza en su lugar `lex.save_zip("birds.zip")`; consulta [Cómo generar archivos LIFT conformes](lift-export-interop.md). diff --git a/docs/es/guides/bulk-edit-glosses.md b/docs/es/guides/bulk-edit-glosses.md new file mode 100644 index 0000000..bb53b97 --- /dev/null +++ b/docs/es/guides/bulk-edit-glosses.md @@ -0,0 +1,79 @@ +# Ejemplo práctico: edición masiva de glosas + +Una tarea habitual de mantenimiento: armonizar la ortografía de todas las entradas en inglés de un léxico (del inglés británico al americano, o viceversa) sin alterar el resto del archivo. En este ejemplo se muestra paso a paso un script que carga, edita, valida y guarda, lo que permite ver cómo funcionan conjuntamente la API de edición y la garantía de fidelidad. + +## El guión + +```python +import sys + +import sil_lift + +path = "dictionary.lift" +lex = sil_lift.load(path) + + +def iter_senses(senses): + """Devuelve cada acepción, incluidas las subacepciones (recursivo).""" + for sense in senses: + yield sense + yield from iter_senses(sense.subsenses) + + +edited_glosses = 0 + +for entry in lex.entries: + for sense in iter_senses(entry.senses): + for glosa en sentido.glosas: + if glosa.idioma != "en": + continue + antiguo = str(glosa.texto) + nuevo = antiguo.replace("colour", "color") + si new != old: + gloss.text = sil_lift.Text([new]) + edited_glosses += 1 + +changed = lex.changed_entries() + +errors = [p for p in lex.iter_problems() si p.level == "error"] +si hay errores: + for problem in errors: + print(problem) + sys.exit(f"abortando: {len(errors)} error(es) de validación, nada guardado") + +lex.save() +print(f"glosa(s) editada(s) {edited_glosses} en {len(changed)} entrada(s)") +``` + +Algunas cosas que conviene destacar: + +- `Sense.subsenses` es en sí mismo una `lista[Sense]`, por lo que `iter_senses` recorre su contenido de forma recursiva; una edición masiva que solo recorriera `entry.senses` omitiría sin avisar cualquier glosa anidada bajo un subsentido. +- `gloss.text` es un `Text`, no una cadena simple: `str(gloss.text)` lo convierte en una cadena para la búsqueda de coincidencias, y la sustitución se vuelve a escribir con `sil_lift.Text([new])` en lugar de modificar la cadena in situ. +- `lex.changed_entries()` indica qué entradas difieren del archivo tal y como se ha cargado. Dado que el resumen de una entrada abarca todo su subárbol, cualquier modificación en un subsignificado anidado se refleja en la entrada que lo contiene. + - Comparando contenido serializado, no se registra el hecho de asignar a un campo el valor que ya tenía. + - Solo informa de los cambios en el contenido; `lex.added_entries()` y `lex.removed_entries()` recogen las entradas que han aparecido o desaparecido desde que se cargó la página. + - Devuelve las propias entradas, sin que les afecte que el `id` esté duplicado o falte (algo que permite LIFT). + - Como recuento, solo tiene sentido cuando hay algo con lo que compararlo. Cuando el escáner de bytes no puede leer la fuente —ya sea por una codificación incompatible con ASCII o por una discrepancia entre el escáner y el analizador—, no existe una línea de referencia, y `changed_entries()` muestra _todas_ las entradas. Esa es la respuesta sincera en lo que respecta a la protección contra escritura, ya que `save()` vuelve a serializar todo el archivo en ese caso, pero eso significa que el recuento corresponde al tamaño del léxico y no al tamaño de la modificación. +- `lex.changes()` indica si el documento ha sufrido algún cambio _en absoluto_. Abarca no solo las entradas, sino también el encabezado, el elemento raíz y todos los elementos asociados a `.lift-ranges`. + - Solo es falso cuando `save()` reproduciría los bytes originales, lo que hace que `if not lex.changes(): ...` sea la forma correcta de omitir una escritura innecesaria. La garantía funciona en un solo sentido: nunca indica «nada que escribir» para un documento que se reescribiría, mientras que un cambio que obligue a una reserialización completa puede volver a los bytes originales y seguir indicándose. + - Compara el contenido, no el destino, por lo que solo debes utilizarlo para guardar en el mismo lugar: `lex.save(some_other_dir / "dictionary.lift")` escribe el documento y sus archivos asociados en una ubicación que aún está vacía, independientemente de si se ha producido algún cambio o no. + - Se trata de una medida de seguridad, no de una optimización: al ejecutarla, se procesan todas las entradas, lo cual equivale al trabajo que realiza `save()` para decidir qué bytes de origen puede reutilizar; por lo tanto, lo que se omite es la propia escritura (sin cambios en la fecha de modificación del archivo, sin diferencias espurias), no el esfuerzo de tomar esa decisión. +- La validación en memoria (`lex.iter_problems()`) serializa primero el estado editado, de modo que este refleje correctamente los cambios antes de que se guarde nada en el disco. Interrumpir la operación ante cualquier `Problem` de nivel `"error"` —las advertencias se dejan a criterio de quien realiza la llamada— significa que una edición incorrecta nunca llega a `save()`. + +Los brillos no son lo único que merece la pena tratar de esta manera. La misma superficie de asignación `Multitext` se aplica a las definiciones y a cualquier otro campo multilingüe de una entrada o un significado: + +```python +sense.definition["en"] = "el color de algo" +``` + +## Ejecutarlo + +Compáralo con un léxico reducido que contenga una entrada y una subentrada que, en ambos casos, indiquen «color»: + +``` +se han editado 2 glosas en 1 entrada +``` + +## La recompensa de la fidelidad + +La garantía se aplica por _entrada_: una entrada cuyo modelo no haya cambiado se devuelve **identica al nivel de bytes** a como se leyó, y solo se vuelven a serializar las entradas en las que realmente se ha realizado algún cambio. En la ejecución anterior, se editaron las glosas de una entrada; el resto de entradas del archivo conservaron sus bytes exactos. (Fíjate en el nivel de detalle: al editar cualquier parte de una entrada, se vuelve a generar el identificador de serie de toda la entrada, incluidos los significados relacionados que no se hayan modificado.) Por lo tanto, al editar una entrada en un léxico de 50 000 entradas, se genera un archivo «diff» que afecta a una sola entrada, y no un archivo reformateado. Consulta [las garantías de Fidelity](../fidelity.md) para conocer los términos exactos del contrato. diff --git a/docs/es/guides/cli.md b/docs/es/guides/cli.md new file mode 100644 index 0000000..4ff7fbc --- /dev/null +++ b/docs/es/guides/cli.md @@ -0,0 +1,70 @@ +# La línea de comandos + +Al instalar el paquete (`pip install sil-lift`) también se instala el comando `sil-lift`, una herramienta compatible con el espíritu de LiftTools que se incluye con el paquete (y, en el caso de `validate`, un ejemplo práctico de la API de la biblioteca). + +``` +sil-lift validate PATH [--format {text,json}] [--strict] [--no-check-media] [--require-ids] + todos los problemas, con archivo/entrada/línea; salida 1 en caso de errores +sil-lift stats PATH [--format {text,json}] + recuentos de entradas/significados/idiomas (en tiempo real; cualquier tamaño) +sil-lift sort PATH [-o OUT] copia ordenada canónicamente y lista para comparar diferencias (por defecto: in situ) +sil-lift check-media PATH informe de medios que faltan y huérfanos; sale con código 1 si faltan +sil-lift export PATH [-o OUT] [--langs L] [--tsv] + una fila por sentido principal (subsentidos aplanados) a CSV/TSV (en streaming) +``` + +`--format json` escribe un único objeto JSON en la salida estándar (y nada más) para su uso en CI/automatización; consulta el esquema del ejemplo que aparece a continuación. `--strict` trata las advertencias como errores y devuelve el valor 1 si se encuentra alguna; utilízalo para condicionar la compilación a que no haya ninguna advertencia, en lugar de basarte únicamente en los errores. `--no-check-media` omite la comprobación de la presencia de archivos multimedia en el sistema de archivos (suprimiendo los resultados de `missing-media`), lo cual resulta útil a la hora de validar una exportación recién generada cuyos archivos de audio o fotos se encuentren en otra ubicación y no en la misma carpeta. `--require-ids` también da error (un error de `missing-id`) en cualquier entrada que carezca de un `guid` o en cualquier sentido que carezca de un `id` — es más estricto que LIFT, para flujos de trabajo que vuelven a importar mediante un identificador estable. Si se pasa `-` como ruta, el documento se lee desde la entrada estándar (un documento transmitido por canalización no tiene carpeta, por lo que su archivo asociado `.lift-ranges` y los archivos multimedia no se resuelven). `stats` también admite la opción `--format json`, con lo que muestra los recuentos en forma de un único objeto JSON. + +!!! note + Los códigos de salida de `validate` y el esquema de `--format json` constituyen una interfaz de automatización compatible: ambos están cubiertos por pruebas y solo cambian según las normas de SemVer. + +`sort` solo reescribe el archivo `.lift`; los archivos complementarios `.lift-ranges` se mantienen sin modificar +(ordénalos por separado con la API `RangesFile`). + +`validate`, `stats`, `check-media` y `export` también admiten un paquete LIFT comprimido (un archivo `.zip` con cualquiera de las dos estructuras: archivos en la raíz del archivo comprimido o anidados dentro de una carpeta de nivel superior); este se extrae a un directorio temporal y se elimina una vez finalizado el comando. Los comandos de streaming `stats` y `export` extraen únicamente el archivo `.lift` en sí, por lo que su ejecución no consume muchos recursos en paquetes con gran cantidad de archivos multimedia; en cambio, `validate` y `check-media` necesitan la carpeta completa y la extraen en su totalidad. + +Ejemplos: + +``` +$ sil-lift validate dictionary.lift +error [dangling-ref] dictionary.lift:88 (entrada apu): la referencia «nope» no coincide con ningún ID de entrada/GUID ni con ningún ID de significado +advertencia [uri-not-rfc] dictionary.lift:6: : Se ha utilizado una letra de unidad de Windows como autoridad URI (estilo FLEx: file://C:/) +1 error(es), 1 advertencia(s) + +$ sil-lift validate dictionary.lift --format json +{ + "problems": [ + { + "level": "error", + "code": "dangling-ref", + "message": "la referencia 'nope' no coincide con ningún ID de entrada/GUID ni ID de sentido", + "file": "dictionary.lift", + "entry_id": "apu", + "guid": null, + "line": 88 + }, + { + "level": "warning", + "code": "uri-not-rfc", + "message": ": Se ha utilizado una letra de unidad de Windows como autoridad URI (estilo FLEx: file://C:/)", + "file": "dictionary.lift", + "entry_id": null, + "guid": null, + "line": 6 + } + ], + «summary»: { + «errors»: 1, + «warnings»: 1 + } +} + +$ sil-lift stats sango.lift +entradas: 3507 +acepciones: 4541 +... + +$ sil-lift export dictionary.lift --langs en,fr -o dictionary.csv +``` + +Códigos de salida: `0`: éxito (se permiten advertencias, salvo si se utiliza la opción `--strict`); `1`: resultados (errores de validación / medios que faltan / advertencias si se utiliza la opción `--strict`); `2`: entrada ilegible. diff --git a/docs/es/guides/folder-media.md b/docs/es/guides/folder-media.md new file mode 100644 index 0000000..f8f4772 --- /dev/null +++ b/docs/es/guides/folder-media.md @@ -0,0 +1,41 @@ +# La carpeta LIFT: gamas y soportes + +Un léxico de LIFT suele ser una _carpeta_: el archivo `.lift`, uno o varios archivos complementarios `.lift-ranges` (archivos «sidecar») y los archivos multimedia de las carpetas `audio/` y `pictures/`. + +## Gamas + +```python +lex = sil_lift.load("dictionary.lift") # los «companions» se registran automáticamente + +lex.ranges_files # {Path(...): RangesFile} +lex.all_ranges() # vista fusionada {id: Range} +lex.all_ranges()["grammatical-info"].elements +``` + +La función «Companion discovery» se adapta al mundo real: se utiliza un `range/@href` que apunta a un archivo existente; los enlaces `file://C:/...` absolutos sin referencia de FieldWorks recurren al nombre base del enlace junto al `.lift`; y el elemento hermano convencional `.lift-ranges` se detecta incluso cuando nada hace referencia a él. + +`lex.save()` guarda el archivo `.lift` y todos los archivos complementarios de los que se lleva un registro. Las modificaciones realizadas en un `RangesFile` se guardan de nuevo en _su_ archivo; los rangos que no se han modificado conservan sus bytes exactos. Uso independiente: + +```python +ranges = sil_lift.RangesFile.load("dictionary.lift-ranges") +ranges.find("grammatical-info") +ranges.sort() +ranges.save() +``` + +Pasa `resolve_ranges=False` a `load()` para omitir la detección de componentes complementarios. + +## Medios de comunicación + +```python +for ref in lex.media_refs(): # todos los y + print(ref.kind, ref.href, ref.entry_id) + +lex.missing_media() # referencias cuyos archivos no existen +``` + +La resolución sigue el esquema convencional: se comprueba un enlace «href» relativo tal y como se ha indicado (con las barras invertidas normalizadas — WeSay escribe «pictures\photo con espacio.png») y dentro de «audio/» (para archivos de audio con pronunciación) o «pictures/» (para ilustraciones). Los enlaces «href» remotos o absolutos no se pueden comprobar y se omiten. + +## Otros contenidos de la carpeta + +Una carpeta LIFT suele contener archivos que sil-lift no modela —el sistema de escritura LDML en `WritingSystems/`, los archivos de audio e imagen de consentimiento de los hablantes de The Combine en `consent/`, y similares—; Las funciones `load()` y `save()` no modifican estos archivos, y [`Lexicon.save_zip()`](lift-export-interop.md) los incluye tal cual al empaquetar la carpeta. diff --git a/docs/es/guides/large-files.md b/docs/es/guides/large-files.md new file mode 100644 index 0000000..20a9cb7 --- /dev/null +++ b/docs/es/guides/large-files.md @@ -0,0 +1,27 @@ +# Archivos de gran tamaño (reproducción en streaming) + +`load()` construye todo el grafo de objetos. En el caso de léxicos de varios cientos de MB, la API de streaming procesa una entrada cada vez en una memoria limitada —el mismo tipo `Entry`—, por lo que el código escrito para un modo funciona también en el otro. + +```python +import sil_lift + +with sil_lift.open_reader("big.lift") as reader: + header = reader.header # analizado al principio (precede a las entradas) + for entry in reader: # iterador perezoso [Entry] + ... +``` + +```python +con sil_lift.open_reader("big.lift") como reader, sil_lift.open_writer( + "out.lift", header=reader.header, producer="my-script" +) como writer: + for entry in reader: + if not entry.date_deleted: # p. ej., eliminar entradas obsoleta + writer.write(entry) +``` + +Notas: + +- El resultado del escritor es exactamente el mismo que produciría el serializador canónico de documento completo para el mismo contenido; los dos modos nunca se desvían entre sí. +- El modo de transmisión no reutiliza ningún byte de origen: la salida es siempre canónica. Los residuos LIFT de nivel raíz —comentarios entre entradas y atributos fuera del esquema en ``— no se transfieren; las entradas y el encabezado están completos, incluidos los residuos. +- Si se produce un error en el cuerpo de un bloque `open_writer`, el archivo queda visiblemente sin terminar (sin el cierre ``); un léxico a medio escribir no debe parecer completo. diff --git a/docs/es/guides/lift-export-interop.md b/docs/es/guides/lift-export-interop.md new file mode 100644 index 0000000..88a62f4 --- /dev/null +++ b/docs/es/guides/lift-export-interop.md @@ -0,0 +1,112 @@ +# Creación de un LIFT conforme + +Esta guía está dirigida a cualquier persona que esté desarrollando un _exportador_ de LIFT, es decir, código en cualquier lenguaje de programación que convierta el modelo de datos de otra aplicación al formato LIFT 0.13. `sil-lift` desempeña dos funciones en ese trabajo: por un lado, actúa como filtro de conformidad que comprueba que la salida se ajuste al esquema y a la semántica que este no puede expresar; y, por otro, sirve de referencia para las formas y las reglas de texto que debe respetar la salida. + +Escribir en LIFT es mucho más fácil que analizarlo sintácticamente: un exportador solo genera el subconjunto de construcciones que produce su propio modelo y nunca tiene que lidiar con todas las opciones de la especificación completa. Lo complicado son los detalles —el complemento `.lift-ranges`, el texto específico para cada sistema de escritura, los identificadores estables y el escape de XML— y eso es precisamente lo que detectan las comprobaciones que se indican a continuación. + +## Paquetes comprimidos + +LIFT suele transportarse como un único archivo `.zip` —tanto FieldWorks como The Combine importan y exportan de esa forma—, por lo que `sil-lift` lee y escribe paquetes comprimidos directamente, independientemente de la estructura que utilice el ecosistema: ya sea con los archivos en la raíz del archivo comprimido o anidados dentro de una carpeta de nivel superior. + +- **Nota:** `sil_lift.load("package.zip")` descomprime el archivo en un directorio temporal, localiza el único archivo `.lift` y lo carga (los archivos complementarios y multimedia se resuelven como de costumbre). + - Los comandos de la CLI `validate`, `stats`, `check-media` y `export` también admiten una ruta `.zip`, por lo que el proceso que se muestra a continuación se ejecuta sobre un paquete tal cual. + - `stats` y `export`, y extraer solo el archivo `.lift` en lugar de todo el paquete, de modo que el coste siga siendo reducido en un paquete con gran volumen de datos multimedia, y el límite de extracción se aplique únicamente al archivo `.lift` y no al resto de elementos que lo rodean. + - La extracción tiene un límite máximo de 10 GiB y 100 000 elementos; cualquier paquete que supere cualquiera de estos límites se rechaza con un `LiftParseError`, al igual que aquellos cuyos caminos de acceso a los elementos se salgan del directorio de extracción. +- **Escribe:** `Lexicon.save_zip("out.zip", wrap_folder="MyDict")` empaqueta el archivo `.lift`, sus archivos `.lift-ranges` y todos los demás archivos de la carpeta de origen (archivos multimedia, `WritingSystems/`, `consent/`, ...) en un archivo zip. + - `wrap_folder` toma por defecto una carpeta de nivel superior cuyo nombre coincide con el del archivo zip (según la convención de importación de FieldWorks/Combine); pasa `False` para obtener un archivo comprimido sin subcarpetas. + +Los archivos `.lift` y `.lift-ranges` conservan su fidelidad a nivel de byte dentro del paquete; el propio contenedor zip no es reproducible a nivel de byte. + +## Validar el resultado como criterio de conformidad + +Dirige el comando «sil-lift validate» al archivo «.lift» generado. Ejecuta RELAX NG (tanto sobre el `.lift` como sobre su complemento `.lift-ranges`) y realiza comprobaciones semánticas que la gramática no puede expresar: referencias sueltas a `relation`/`variant`, GUID duplicados, integridad del elemento padre del rango, valores de rasgos e información gramatical no definidos en su rango, y referencias del encabezado `range/@href` que no se resuelven en ningún complemento. + +En el caso de la CI, si se produce un error en cualquier paso, se deben generar resultados legibles por máquina: + +``` +sil-lift validate export.lift --strict --no-check-media --format json +``` + +- La opción `--strict` hace que las advertencias (y no solo los errores) provoquen el fallo de la ejecución. +- `--no-check-media` omite la comprobación de la presencia de archivos multimedia en el sistema de archivos, cuyos resultados de `missing-media` son falsos positivos cuando los archivos de audio o fotos no se encuentran en la misma carpeta que el archivo `.lift` en la integración continua (CI). +- `--format json` muestra un único objeto JSON (`{"problems": [...], "summary": {...}}`) en lugar de texto legible para el usuario; sus códigos de salida y su esquema constituyen una interfaz compatible y sujeta a SemVer (véase [la guía de la línea de comandos](cli.md)). +- `--require-ids` genera además un error si hay entradas a las que les falte un `guid` o detecta que faltan `id`s; esto resulta útil cuando, en una reimportación posterior, es necesario actualizar los datos en lugar de duplicarlos. + +Evita la pérdida silenciosa de datos (el modo de fallo que hace que la exportación a CSV plano conlleve pérdidas) comprobando los recuentos con `stats --format json` en tu modelo de origen: + +``` +sil-lift stats export.lift --format json +``` + +Muestra los recuentos de «entradas», «significados», «ejemplos», «referencias multimedia», «idiomas» y «características» por nombre. + +### Ejecutar Gate sin el entorno de desarrollo de Python + +La integración continua (CI) de un proyecto de TypeScript o C# puede ejecutar la misma comprobación sin necesidad de instalar Python, mediante la acción de GitHub incluida: + +```yaml +- uses: sillsdev/python-sil-lift@v0.1.0 + with: + path: export.lift + strict: "true" + no-check-media: "true" + format: json +``` + +o la imagen del contenedor, creada a partir del `Dockerfile` del repositorio: + +``` +docker build -t sil-lift . +docker run --rm -v "$PWD:/work" -w /work sil-lift validate export.lift --strict +``` + +## El complemento `.lift-ranges` + +Los vocabularios controlados —partes del discurso, dominios semánticos y cualquier otro conjunto de valores basados en rasgos— se encuentran en un archivo `.lift-ranges` asociado, al que se hace referencia desde el archivo `
`: + +```xml +
+ + + + +
+``` + +La guía incluye la descripción completa de cada gama. Los valores son « »; «parent» establece una jerarquía; «label», «abbrev» y «description» son textos múltiples: + +```xml + + + + + +
n
+
+
+ + + + + +
+``` + +A continuación, una entrada hace referencia a un valor mediante su identificador: la categoría gramatical de un sentido es ``, y un dominio semántico es ``. `sil-lift validate` muestra una advertencia (`undefined-range-value`) cuando un valor no está definido en su rango y genera un error (`range-parent`) cuando un `parent` no es un identificador de elemento hermano; por lo tanto, indica los rangos que tus datos utilizan realmente. Véase también [Rangos y medios](folder-media.md). + +Si creas la exportación en Python, `Lexicon.add_ranges_file()`, `RangesFile.add_range()` y `Range.add_element()` construyen el archivo complementario y añaden las referencias de encabezado por ti; `open_writer(..., ranges=...)` hace lo mismo en la ruta de transmisión. + +## Texto y multitexto + +Cada cadena de lenguaje humano en LIFT es un _multitexto_: un `
` por sistema de escritura, cada uno de los cuales envuelve un ``: + +```xml + + kanga +
gallina
+
+``` + +Un modelo que indexa cadenas por código de idioma (un `MultiString`, un `Record`, un `dict[str, str]`) se corresponde con este de forma biunívoca: cada entrada por clave se convierte en un `
`. En un mismo multitexto solo se permite una forma por idioma; de lo contrario, `sil-lift` muestra el aviso `duplicate-form-lang`. + +El escape de XML es la única parte en la que realmente importa la precisión. En el texto de los elementos, los caracteres `&`, `<`, and `>` deben escaparse (`&`, `<`, `>`); en los valores de los atributos, también debe escaparse el carácter de comilla. El autor de `sil-lift` aplica exactamente estas reglas y nunca modifica los espacios en blanco dentro de ``: no añade sangría en ese lugar, ya que eso corrompería los datos léxicos. Si quieres que el resultado sea idéntico, reutiliza el escape de un serializador XML auténtico (no una sustitución hecha a mano en la que se olvide el símbolo `&`) y deja el contenido de `` tal y como aparece en el archivo original, byte a byte. diff --git a/docs/es/guides/read-edit-write.md b/docs/es/guides/read-edit-write.md new file mode 100644 index 0000000..75bad19 --- /dev/null +++ b/docs/es/guides/read-edit-write.md @@ -0,0 +1,66 @@ +# Leer, editar, escribir + +## Cargando + +```python +import sil_lift + +lex = sil_lift.load("dictionary.lift") +``` + +La función `load()` admite cualquier documento LIFT **0.13** bien formado, incluidos los archivos reales que no cumplen con el esquema. Todo aquello que el modelo no defina (elementos o atributos desconocidos, comentarios) se transmite sin pérdida de información como residuo LIFT en el campo opaco `extra` de cada nodo. Otras versiones de LIFT generan un error `LiftParseError` indicando la versión. + +## El modelo + +Cada elemento de LIFT es una clase de datos tipada: `Entry`, `Sense`, `Example`, `Pronunciation`, `Variant`, `Relation`, `Etymology`, `Reversal`, etc. Un texto multilingüe es un `Multitext`, que se comporta como una correspondencia entre un código de idioma y un `Text`: + +```python +entry = lex.find(id="abat") + +str(entry.lexical_unit["seh"]) # "abat" +entry.lexical_unit["en"] = "grove" # las cadenas simples se convierten +"en" in entry.citation # False +``` + +El `texto` está estructurado —una lista ordenada de fragmentos `str` y `Span`— porque `` puede contener marcado anidado ``. `str(text)` convierte el contenido en texto sin formato; los fragmentos conservan el marcado para facilitar la conversión de ida y vuelta. + +En LIFT, los glosas tienen forma de _forma_ (cada `` tiene su propio lenguaje), por lo que un sentido tiene `glosses: list[Form]`, además de una función auxiliar: + +```python +sense = entry.senses[0] +sense.gloss("en") # Texto | None +entry.gloss_langs() # {"en", "id"} +``` + +## Ahorro + +```python +lex.save() # volver al lugar desde donde se cargó +lex.save("elsewhere.lift") +``` + +Las entradas que no hayas modificado se vuelven a escribir **con los mismos bytes**; un documento que no hayas modificado en absoluto es idéntico, byte a byte, desde el primer byte hasta el último. Consulta [las garantías de Fidelity](../fidelity.md) para conocer los términos exactos del contrato. + +## Construir desde cero + +```python +lex = sil_lift.Lexicon(producer="my-script 1.0") +entry = sil_lift.Entry(id="hello", guid="...") +entry.lexical_unit["en"] = "hello" +sense = sil_lift.Sense() +sense.glosses.append(sil_lift.Form("fr", sil_lift.Text(["bonjour"]))) +entry.senses.append(sense) +lex.entries.append(entry) +lex.save("new.lift") +``` + +## Ordenación canónica + +```python +lex.sort() # entradas ordenadas por (guid, id); rangos/definiciones de campos por id/etiqueta +lex.save() # las entradas no modificadas conservan sus bytes exactos, en el nuevo orden + +sil_lift.canonicalize("in.lift", "out.lift") # totalmente reserializado, listo para la comparación de diferencias +``` + +Véase también: [Ejemplo práctico: edición masiva de glosas](bulk-edit-glosses.md). diff --git a/docs/es/guides/validate.md b/docs/es/guides/validate.md new file mode 100644 index 0000000..8c94ab9 --- /dev/null +++ b/docs/es/guides/validate.md @@ -0,0 +1,36 @@ +# Validar + +La validación siempre es explícita: las operaciones de carga y guardado nunca realizan una validación implícita. + +```python +import sil_lift + +# Exhaustivo: un flujo diferido de problemas (esquema + capas semánticas). +for problem in sil_lift.iter_problems("dictionary.lift"): + print(problem) + # error [dangling-ref] dictionary.lift:88 (entrada apu): la referencia «nope» coincide con... + +# Detección rápida de errores: lanza un LiftValidationError ante el primer problema de nivel de error. +sil_lift.validate_file("dictionary.lift") + +# Estado en memoria (se serializa primero — un coste documentado en léxicos grandes): +lex = sil_lift.load("dictionary.lift") +problems = list(lex.iter_problems()) +``` + +Cada «Problema» incluye un «nivel» («error»/«advertencia»), un «código» fijo, un «mensaje» y una dirección: «archivo», «id_entrada», «guid» y «línea». + +## Las capas + +1. **RELAX NG** con respecto a la gramática LIFT 0.13 (incluida en lift-standard —una copia idéntica al byte, incorporada a este paquete). +2. **Esquema de rangos** —el archivo `lift-ranges-0.13.rng` de este proyecto— sobre cada complemento `.lift-ranges` al que se realiza un seguimiento. +3. **Comprobaciones semánticas** que la gramática no puede expresar: `duplicate-guid`, `dangling-ref`, `range-parent`, `undefined-range-value`, `duplicate-form-lang`, `missing-media`. + +## Resultados de FieldWorks (FLEx) en el mundo real + +FieldWorks genera de forma sistemática cierto contenido que las herramientas más estrictas rechazan. A continuación se expone la política de sil-lift, para que los léxicos reales resulten útiles: + +- Los enlaces `file://C:/...` (URI no válidos) se señalan como **advertencias** (`uri-not-rfc`), no como errores de esquema; el validador de C# nunca los ha rechazado. +- Los elementos secundarios intercalados legalmente (por ejemplo, «campo, nota, campo, nota», en cierto sentido) **no** se marcan, lo que permite evitar un falso positivo en libxml2. +- Los valores de rango se comparan según la normalización NFC de Unicode: FLEx escribe el archivo `.lift` en NFC, pero el `.lift-ranges` en NFD dentro de la misma exportación. +- Las extensiones `trait`/`field` de FLEx dentro de `range-element` **sí** se señalan (como errores de esquema respecto al esquema de rangos): se trata de auténticas desviaciones respecto a la especificación. diff --git a/docs/es/index.md b/docs/es/index.md new file mode 100644 index 0000000..50678f9 --- /dev/null +++ b/docs/es/index.md @@ -0,0 +1,32 @@ +# sil-lift + +Una biblioteca de Python para [LIFT](https://github.com/sillsdev/lift-standard) (Lexicon Interchange FormaT) 0.13: lectura y escritura sin pérdidas de la carpeta LIFT (`.lift` + `.lift-ranges` + referencias a medios), validación semántica y del esquema, y ordenación canónica, con API de streaming para léxicos de gran tamaño. + +**Estado: versión preliminar, en fase de desarrollo activo.** + +## Instalar + +De [PyPI](https://pypi.org/project/sil-lift/): + +``` +pip install sil-lift # biblioteca + el comando sil-lift +``` + +Requiere Python 3.11 o superior. La única dependencia de ejecución es lxml. + +## El recorrido de 30 segundos + +```python +import sil_lift + +lex = sil_lift.load("thesaurus.lift") # también rastrea los términos asociados a los rangos .lift + +for entry in lex.entries: + if "en" not in entry.gloss_langs(): + print(entry.id, str(entry.lexical_unit.get("seh") or "")) + +entry = lex.find(guid="0f5a9c3e-...") # o lex.find(id="hoofd_a1b2") +entry.senses[0].definition["en"] = "head (anatomy)" + +lex.save() # las entradas no modificadas son idénticas a nivel de bytes; la entrada editada se vuelve a serializar +``` diff --git a/docs/es/reference.md b/docs/es/reference.md new file mode 100644 index 0000000..05ea450 --- /dev/null +++ b/docs/es/reference.md @@ -0,0 +1,9 @@ +# Referencia de la API + +La API pública es exactamente lo que `sil_lift` reexporta en el nivel superior; los módulos con el prefijo «_» son detalles de implementación. + +::: sil_lift +options: +show_root_heading: false +show_source: false +members_order: source diff --git a/docs/fr/csharp-differences.md b/docs/fr/csharp-differences.md new file mode 100644 index 0000000..f05d019 --- /dev/null +++ b/docs/fr/csharp-differences.md @@ -0,0 +1,52 @@ +# Différences par rapport aux bibliothèques C\# + +sil-lift s'apparente vaguement aux outils LIFT de SIL en C# — principalement `SIL.Lift` dans [libpalaso](https://github.com/sillsdev/libpalaso) (analyseur syntaxique, validateur, migrateur, `LiftSorter`), `SIL.DictionaryServices` dans le même dépôt (le modèle `LexEntry`/`LexSense`, doté de son propre lecteur/enregistreur LIFT, utilisé par The Combine et WeSay), ainsi que les gestionnaires LIFT de [Chorus](https://github.com/sillsdev/chorus). Il s'agit d'une nouvelle implémentation, et non d'un portage. Cette page résume les points sur lesquels le comportement diffère délibérément. + +## Champ d'application + +| Capacité | Bibliothèques C# | sil-lift | +| -------------------------------------- | ---------------------------------------------------- | ---------------------------------------------------------------------------------------------------------- | +| Versions de LIFT | 0,10–0,13 (migration intégrée) | **0.13 uniquement** ; les versions antérieures sont rejetées et génèrent une erreur claire | +| Migration de version | `Migrator` (chaîne XSLT) | aucun — utiliser les fichiers XSLT du répertoire « lift-standard » pour les mises à jour ponctuelles | +| Fusion / synchronisation à trois voies | Refrain | hors du champ d'application | +| Validation | RELAX NG uniquement (`Validator`) | RELAX NG + vérifications du schéma et de la sémantique | +| Streaming | analyse syntaxique interne à granularité d'entrée | API publique `open_reader` / `open_writer` | + +## Forme de l'API + +Le parseur de `SIL.Lift` fonctionne par callbacks (`ILexiconMerger`) : il transmet les événements d'analyse à un consommateur. sil-lift renvoie en revanche un simple graphe d'objets — des classes de données typées pour chaque élément LIFT — car les développeurs Python souhaitent des objets, et non des callbacks. `SIL.DictionaryServices` superpose effectivement un modèle d'objets `LexEntry`/`LexSense` à `SIL.Lift`, mais en tant que modèle d’application, il ne représente que les constructions utilisées par ces applications — par conséquent, la resérialisation via ce modèle ne permet pas de préserver le contenu hors modèle, contrairement à la gestion des résidus LIFT et à la fidélité au niveau des octets offertes par sil-lift (voir ci-dessous). L'API de streaming renvoie le _même_ type `Entry` ; il n'y a donc pas de deuxième modèle simplifié à apprendre. + +## Fidélité aller-retour + +La différence la plus marquée et la plus délibérée. L'enregistrement avec `SIL.Lift` entraîne la resérialisation de l'intégralité du document. sil-lift garantit : + +- un document inchangé est enregistré **avec une taille en octets identique**, et +- Les entrées inchangées conservent exactement les mêmes octets source, même lorsque d'autres entrées sont modifiées — il s'agit du même découpage en blocs d'octets par entrée que celui utilisé par Chorus, appliqué automatiquement. + +Consultez les [garanties de fidélité](fidelity.md). + +## Validation + +Le `Validator` C# effectue un passage RELAX NG et renvoie les premières erreurs sous forme de chaînes de caractères. sil-lift génère un flux structuré de « problèmes », chacun contenant le fichier, l'entrée et la ligne concernés, et son schéma présente sciemment trois divergences : + +- **Les URI non valides constituent des avertissements, et non des erreurs.** Le moteur RELAX NG de C# n'a jamais imposé le type de données `anyURI` ; c'est pourquoi FieldWorks (FLEx) insère depuis des années des liens `file://C:/...` dans des lexiques réels. Le rejet de ces fichiers entraînerait le marquage de pratiquement toutes les exportations FLEx. +- **Les règles Schematron sont appliquées** (sous forme de vérifications sémantiques) : les langages de formulaire en double et les co-contraintes similaires présentes dans la grammaire LIFT étaient ignorés sans avertissement tant par la validation en C# que par celle effectuée directement avec lxml. +- **Les comparaisons entre fichiers sont normalisées selon la norme Unicode**, car FLEx enregistre le fichier `.lift` en NFC et le fichier associé `.lift-ranges` en NFD. + +sil-lift valide également les fichiers `.lift-ranges` associés à un lexique chargé par rapport à un schéma destiné aux documents de plages autonomes (fourni par `lift-standard` avec la grammaire LIFT de base) — chaque fichier de plages externe suivi est vérifié à chaque validation du `.lift` — alors qu’il n’existe aucun schéma (ni vérification) de ce type dans l’univers C#. (Il n'existe aucun moyen de valider un fichier `.lift-ranges` isolément, sans qu'il soit associé à un fichier `.lift`.) + +## Tri canonique + +La méthode `Lexicon.sort()` reprend les règles fondamentales de `LiftSorter` (entrées classées par GUID sans distinction de majuscules/minuscules ; plages et éléments de plage classés par ID ; définitions des champs d'en-tête classées par balise ; sens conservés dans l'ordre du fichier ; espaces à l'intérieur de `` jamais modifiés), à trois différences près : + +- Les entrées sans GUID sont triées de manière déterministe par ID (la classe `LiftSorter` part du principe qu'un GUID est présent) ; +- l'ordre est indépendant des paramètres régionaux (points de code sans distinction de casse, et non le classement « invariant-culture » de .NET) ; +- Les listes de même type, telles que les notes, les relations et les formulaires, conservent leur ordre d'origine dans le document au lieu d'être triées à nouveau par clé — le regroupement est déjà déterministe, et les réorganiser ne ferait qu'ajouter du bruit aux différences. + +Le fichier `canonicalizeLift.xsl` du dépôt de spécifications n'est absolument pas utilisé : il supprime les espaces blancs à l'intérieur du texte lexical (opération destructive) et les identifiants qu'il génère varient à chaque exécution. + +## Non reporté + +- Fonctionnalités spécifiques à WeSay (tableau de bord / gestion des paramètres liés aux fichiers LIFT). +- `SynchronicMerger` (fusion des mises à jour Chorus) — le principe du découpage en blocs d'octets est conservé dans la couche de fidélité, mais pas celui de la fusion. +- Analyse syntaxique du système d'écriture LDML : les fichiers situés dans le répertoire `WritingSystems/` sont considérés comme du contenu de dossier opaque. diff --git a/docs/fr/fidelity.md b/docs/fr/fidelity.md new file mode 100644 index 0000000..0be4e1b --- /dev/null +++ b/docs/fr/fidelity.md @@ -0,0 +1,33 @@ +# Garanties de fidélité + +LIFT est un format d'_échange_ ; la première règle est donc de **ne jamais ignorer ce que vous ne comprenez pas**. Le contrat de `sil-lift`, vérifié par la suite de tests à chaque exécution (fichiers de corpus et génération basée sur les propriétés) : + +## Lecture + +Tout document LIFT 0.13 bien formé se charge, même s'il contient du contenu non conforme au schéma. Tout ce qui n’est pas défini par le modèle est transféré dans le conteneur opaque `Extras` du nœud le plus proche sous la forme d’un « résidu LIFT » — nom donné par FieldWorks à ce concept, qu’il stocke dans un champ `LiftResidue` : attributs et éléments inconnus, commentaires XML et instructions de traitement, texte parasite et attributs typés mal formés (une date incorrecte reste sous forme de chaîne d’origine dans `Extras` ; le champ de type est `None`). + +## Enregistrer un document sans modification + +`load()` → `save()` sans modification génère une **sortie identique au niveau des octets** — pas de reformatage, pas de ré-échappement, pas de réorganisation ; les marques d'ordre des octets et les déclarations XML sont incluses. Il n'existe actuellement aucune liste de normalisation : l'identité est exacte. + +Exceptions (le programme de lecture revient à la sérialisation canonique complète, qui est sémantiquement complète mais ne préserve pas les octets) : + +- le codage source n'est pas compatible ASCII (il ne s'agit ni d'UTF-8 ni d'US-ASCII), ou +- le code source contient une déclaration DOCTYPE, ou +- le scanner d'octets et l'analyseur syntaxique ne s'accordent pas sur la structure de haut niveau du document — par exemple, un deuxième `
` non conforme à la spécification, que l'analyseur syntaxique ne conserve qu'une seule fois (le scanner est délibérément prudent : en cas de doute, il ne capture aucun octet de la source), ou +- le code source a été généré en mémoire plutôt que chargé à partir d'un fichier. + +## Enregistrer un document modifié + +- **Les entrées non modifiées sont transmises telles quelles, à partir de leurs octets d'origine.** Une entrée est considérée comme modifiée si une partie quelconque de son objet modèle a changé depuis l'analyse (ce qui est détecté par l'instantané de sérialisation canonique, et non par un indicateur de modification). +- **Les entrées modifiées sont resérialisées de manière canonique et complète** : UTF-8, indentation à 2 espaces _en dehors_ du contenu mixte (les espaces à l'intérieur de `` et `` ne sont jamais modifiés), un regroupement des éléments enfants documenté pour chaque élément (par exemple, entrée : unité lexicale, citation, prononciations, variantes, sens, notes, relations, étymologies, annotations, traits, champs), ordre fixe des attributs, dates au format ISO-8601 (`Z` pour l'UTC). Tous les résidus sont réémis ; leur position est rétablie dans l'index enfant d'origine, en étant ancrée à la nouvelle liste des enfants (il s'agit d'une approximation — les positions exactes en octets ne sont garanties que pour les entrées non modifiées). +- L'ajout, la suppression ou le réordonnancement d'entrées entraîne une nouvelle sérialisation de la structure du document, mais les octets de chaque entrée inchangée sont toujours restitués à l'identique. + +!!! note ""Le fichier XML canonique" ne fait référence à aucun autre fichier XML canonique." + Sur cette page, on entend par « forme canonique » la forme propre à `sil-lift`, telle qu'elle est décrite dans l'un des points ci-dessus. Cela n'a aucun rapport avec le processus « Canonical XML (C14N) » du W3C. Cela n'a aucun rapport avec la classe `CanonicalXmlSettings` de `SIL.Core`. + +## Approximations connues (nœuds touchés uniquement) + +- Les commentaires situés _à l'intérieur_ d'une exécution `` sont conservés, mais déplacés à côté de l'exécution, et non pas maintenus à leur position exacte dans le texte. +- L'ordre croisé des enfants au sein d'un élément modifié est normalisé selon le regroupement canonique (la propriété `interleave` du schéma LIFT rend cet ordre sans importance sur le plan sémantique). +- Un élément multitext présent mais ne contenant rien — ni forme, ni résidu, par exemple `` — n'est pas réémis. Le modèle représente ces champs sous la forme d'un `Multitext` toujours présent (`lexical-unit`, `citation`, `definition`, l'`usage` d'une relation et `label` / `abbrev` / `description` sur les références URL, les intervalles, les éléments d’intervalle et l’en-tête), de sorte qu’un champ vide est impossible à distinguer d’un champ absent après analyse. Il n'y a aucune perte sémantique. diff --git a/docs/fr/guides/build-export.md b/docs/fr/guides/build-export.md new file mode 100644 index 0000000..9c9299a --- /dev/null +++ b/docs/fr/guides/build-export.md @@ -0,0 +1,168 @@ +# Exemple pratique : créer une exportation LIFT à partir de zéro + +Si vous exportez les données d'une autre application au format LIFT — ce qui correspond à la tâche décrite dans [« Produire un LIFT conforme »](lift-export-interop.md) —, `sil-lift` peut construire le document objet par objet et le sérialiser, au lieu de générer manuellement du code XML. Ce guide présente un script qui construit une entrée comprenant les éléments caractéristiques d'un véritable dictionnaire (plusieurs systèmes d'écriture, une prononciation, un sens accompagné d'un exemple, une illustration, un trait de domaine sémantique et un champ spécifique à l'application), écrit les vocabulaires contrôlés dans un fichier compagnon `.lift-ranges`, effectue une validation, puis enregistre le tout. + +## Le scénario + +```python +from pathlib import Path + +import sil_lift + +lex = sil_lift.Lexicon(producer="my-exporter") + +# Une entrée, construite à partir du modèle source. +entry = sil_lift.Entry(id="kanga", guid="6b9e7c2a-3f4d-4a1b-8c5e-2d9f0a1b2c3d") +entry.lexical_unit["seh"] = "nkhuku" +entry.lexical_unit["pt"] = "galinha" + +pron = sil_lift.Pronunciation() +pron.forms["en"] = "Speaker: Ana" # Convention de nommage des locuteurs du Combine +pron.media.append(sil_lift.URLRef(href="audio/nkhuku.wav")) +entry.pronunciations.append(pron) + +sense = sil_lift.Sense(id="kanga_s1") +sense.grammatical_info = sil_lift.GrammaticalInfo(value="Noun") +sense.glosses.append(sil_lift.Form(lang="en", text=sil_lift.Text(["chicken"]))) +sense.definition["en"] = "une volaille domestique élevée pour ses œufs et sa viande" + +example = sil_lift.Example() +example.forms["seh"] = "Ndinafuna nkhuku." +translation = sil_lift.Translation() +translation.forms["en"] = "I want a chicken." +example.translations.append(translation) +sense.examples.append(example) + +photo = sil_lift.URLRef(href="pictures/hen.jpg") +photo.label["en"] = "Une poule" +sense.illustrations.append(photo) + +sense.traits.append(sil_lift.Trait(name="semantic-domain-ddp4", value="1.6.1.2")) + +scientific = sil_lift.Field(type="scientific-name") # un champ supplémentaire spécifique à l’application +scientific.content["en"] = "Gallus gallus domesticus" +sense.fields.append(scientific) + +entry.senses.append(sense) +lex.entries.append(entry) + +# Les vocabulaires contrôlés auxquels l'entrée fait référence, dans un fichier .lift-ranges associé. +ranges = sil_lift.RangesFile() +ranges.add_range("grammatical-info").add_element("Noun").label["en"] = "noun" +ranges.add_range("semantic-domain-ddp4").add_element("1.6.1.2").label["en"] = "Bird" +lex.add_ranges_file(ranges, href="birds.lift-ranges") + +# Valider ce que save() écrirait, avant d'écrire sur le disque. +problems = list(lex.iter_problems()) +print(f"validation : {len(problems)} problème(s)") + +out = Path("export") +out.mkdir(exist_ok=True) +lex.save(out / "birds.lift") +print("=== birds.lift ===") +print((out / "birds.lift").read_text(encoding="utf-8"), end="") +print("=== birds.lift-ranges ===") +print((out / "birds.lift-ranges").read_text(encoding="utf-8"), end="") +``` + +## Ce qu'il produit + +`validation : 0 problème(s)`, puis le `.lift` et son équivalent côte à côte : + +``` +=== birds.lift === + + +
+ + + + +
+ + + + nkhuku + +
+ galinha +
+
+ +
+ Speaker: Ana +
+ +
+ + + + chicken + + +
+ a domestic fowl kept for its eggs and meat +
+
+ +
+ Ndinafuna nkhuku. +
+ +
+ I want a chicken. +
+
+
+ + + + + +
+ Gallus gallus domesticus +
+
+
+
+
+=== birds.lift-ranges === + + + + + + + + + + + + + +``` + +## Remarques sur l'API + +- Champs multitexte (`lexical_unit`, `definition`, une étiquette `Form`/`URLRef`, le contenu d'un `Field`, etc.) Prendre une chaîne par système d'écriture via l'interface de mappage : `entry.lexical_unit["seh"] = "nkhuku"` ajoute un `
`. Un modèle source qui indexe les chaînes de caractères par code de langue s'adapte parfaitement à cette structure. +- `RangesFile.add_range()` / `Range.add_element()` permettent de créer les vocabulaires contrôlés, tandis que `Lexicon.add_ranges_file(ranges, href=...)` associe le fichier correspondant et ajoute les références d’en-tête ``, de sorte que les entrées `` et `` renvoient aux plages que vous avez définies. +- Un `URLRef` est un attribut `href` accompagné d'un texte multiple facultatif (légende ou étiquette) — utilisé à la fois pour `` (audio) et `` (photos). La prononciation suivie ici respecte la convention de The Combine, qui prévoit une forme « en » se lisant « Intervenant : ». +- Les données spécifiques à l'application ne comportant pas de trajets de retour LIFT natifs, sous la forme « » (ou « ») : FieldWorks les interprète comme des champs personnalisés et The Combine les conserve. +- Attribuez à chaque entrée un `guid` réel et stable (généré par exemple par `uuid.uuid4()`, réutilisé d'une exportation à l'autre) : une réimportation ultérieure mettra à jour l'entrée sur place plutôt que de la dupliquer. La commande `sil-lift validate --require-ids` garantit le respect de cette règle. +- La fonction `lex.iter_problems()` valide le document en mémoire (ce que la fonction `save()` écrirait) avant que quoi que ce soit ne soit enregistré sur le disque ; ici, il est correct. Comme le lexique ne dispose pas encore de dossier, les vérifications « media-presence » et « companion-href » sont ignorées — exécutez [`sil-lift validate`](cli.md) sur le fichier de sortie enregistré (ou avec l'option `--no-check-media`) une fois que les fichiers audio et photo sont en place. + +## Emballage + +La commande `lex.save("export/birds.lift")` enregistre le dossier sous la forme (fichiers `.lift` et `.lift-ranges` côte à côte). Pour générer un seul fichier compressé que FieldWorks et The Combine importent directement, utilisez plutôt `lex.save_zip("birds.zip")` — voir [Création de fichiers LIFT conformes](lift-export-interop.md). diff --git a/docs/fr/guides/bulk-edit-glosses.md b/docs/fr/guides/bulk-edit-glosses.md new file mode 100644 index 0000000..af7585d --- /dev/null +++ b/docs/fr/guides/bulk-edit-glosses.md @@ -0,0 +1,79 @@ +# Exemple pratique : modification groupée des gloses + +Une tâche de maintenance courante : harmoniser l'orthographe de toutes les entrées en anglais d'un lexique (anglais britannique → anglais américain, ou inversement) sans modifier aucun autre élément du fichier. Cet exemple présente un script qui charge, modifie, valide et enregistre des données, illustrant ainsi le fonctionnement conjoint de l'API d'édition et de la garantie de fidélité. + +## Le scénario + +```python +import sys + +import sil_lift + +path = "dictionary.lift" +lex = sil_lift.load(path) + + +def iter_senses(senses): + """Renvoie chaque sens, y compris les sous-sens (récursif).""" + for sense in senses: + yield sense + yield from iter_senses(sense.subsenses) + + +edited_glosses = 0 + +for entry in lex.entries: + for sense in iter_senses(entry.senses): + for gloss in sense.glosses: + if gloss.lang != "en": + continue + old = str(gloss.text) + new = old.replace("colour", "color") + if new != old : + gloss.text = sil_lift.Text([new]) + edited_glosses += 1 + +changed = lex.changed_entries() + +errors = [p for p in lex.iter_problems() if p.level == "error"] +if errors : + for problem in errors: + print(problem) + sys.exit(f"interruption : {len(errors)} erreur(s) de validation, rien n'a été enregistré") + +lex.save() +print(f" {edited_glosses} glossaire(s) modifié(s) sur {len(changed)} entrée(s)") +``` + +Quelques points à retenir : + +- `Sense.subsenses` est lui-même une `list[Sense]` ; ainsi, `iter_senses` effectue une itération récursive sur cet élément — une modification groupée qui ne parcourrait que `entry.senses` omettrait sans avertissement tout glossaire imbriqué sous un sens secondaire. +- `gloss.text` est un objet `Text`, et non une simple chaîne de caractères : `str(gloss.text)` l'aplatit pour permettre la correspondance, et le remplacement est réécrit à l'aide de `sil_lift.Text([new])` plutôt que de modifier la chaîne en place. +- La fonction `lex.changed_entries()` indique quelles entrées diffèrent du fichier tel qu'il a été chargé. Étant donné que le résumé d'une entrée couvre l'ensemble de sa sous-arborescence, toute modification apportée à une sous-signification imbriquée se répercute sur l'entrée qui la contient. + - Comme il compare du contenu sérialisé, le fait d'attribuer à un champ la valeur qu'il avait déjà n'est pas signalé. + - Elle ne signale que les modifications de contenu ; les fonctions `lex.added_entries()` et `lex.removed_entries()` concernent les entrées qui sont apparues ou ont disparu depuis le chargement. + - Elle renvoie les entrées elles-mêmes, sans tenir compte du fait que l'`id` puisse être dupliqué ou absent (ce que permet LIFT). + - En tant que chiffre, il n'a de sens que s'il existe un élément de comparaison. Lorsque le scanner d'octets refuse de lire la source — en raison d'un encodage non compatible ASCII ou d'un désaccord entre le scanner et l'analyseur —, il n'y a pas de référence, et la fonction `changed_entries()` signale _toutes_ les entrées. C'est la réponse honnête concernant la protection en écriture, puisque `save()` resérialise l'intégralité du fichier dans ce cas, mais cela signifie que le nombre correspond à la taille du lexique plutôt qu'à celle de la modification. +- La fonction `lex.changes()` indique si le document a subi la moindre modification. Cela concerne non seulement les entrées, mais aussi l'en-tête, l'élément racine et tous les éléments associés à `.lift-ranges`. + - Cette condition n'est fausse que lorsque la fonction `save()` reproduirait les octets source, ce qui fait que `if not lex.changes(): ...` est la bonne façon d'éviter une écriture inutile. Cette garantie fonctionne dans un seul sens : elle ne signale jamais « rien à écrire » pour un document qui serait réécrit, tandis qu'une modification imposant une resérialisation complète peut aboutir aux mêmes octets d'origine et être tout de même signalée. + - Comme cette fonction compare le contenu et non la destination, utilisez-la uniquement pour enregistrer le document à l'emplacement actuel : `lex.save(some_other_dir / "dictionary.lift")` écrit le document et ses fichiers associés dans un emplacement encore vide, qu'il y ait eu des modifications ou non. + - Il s'agit d'une mesure de sécurité, pas d'un gain de vitesse : l'exécution de cette commande analyse chaque entrée, ce qui correspond au travail effectué par la fonction `save()` pour déterminer quels octets source peuvent être réutilisés. Ce que vous évitez, c'est donc l'écriture proprement dite (une date de modification du fichier inchangée, pas de différence erronée), et non l'effort de prise de décision. +- La validation en mémoire (`lex.iter_problems()`) sérialise d'abord l'état modifié, ce qui permet de s'assurer qu'il reflète correctement les modifications avant que quoi que ce soit ne soit écrit sur le disque. L'interruption en cas de `Problem` de niveau « erreur » — les avertissements sont laissés à l'appréciation de l'appelant — garantit qu'une modification incorrecte n'atteindra jamais la fonction `save()`. + +Les gloss ne sont pas les seuls produits qui méritent d'être appliqués de cette manière. La même surface de mappage `Multitext` s'applique aux définitions et à tous les autres champs multilingues d'une entrée ou d'un sens : + +```python +sense.definition["en"] = "la couleur d'un objet" +``` + +## L'exécuter + +Effectuez une recherche dans un petit lexique comportant une définition et une sous-définition qui indiquent toutes deux « couleur » : + +``` +modification de 2 termes dans 1 entrée +``` + +## Les avantages de la fidélité + +La garantie s'applique à chaque _entrée_ : une entrée dont le modèle n'a pas changé est restituée **à l'identique au niveau des octets** par rapport à sa forme d'origine, et seules les entrées sur lesquelles vous avez effectivement intervenu sont à nouveau sérialisées. Dans l'extrait ci-dessus, les gloses d'une entrée ont été modifiées — toutes les autres entrées du fichier ont conservé exactement les mêmes octets. (Notez le niveau de détail : la modification d'une partie quelconque d'une entrée entraîne la resérialisation de l'intégralité de cette entrée, y compris ses sens apparentés qui n'ont pas été modifiés.) La modification d'un terme dans un lexique de 50 000 entrées génère donc un fichier « diff » ne concernant qu'une seule entrée, et non un fichier reformaté. Pour consulter le contrat dans son intégralité, voir [Garanties Fidelity](../fidelity.md). diff --git a/docs/fr/guides/cli.md b/docs/fr/guides/cli.md new file mode 100644 index 0000000..82f159c --- /dev/null +++ b/docs/fr/guides/cli.md @@ -0,0 +1,70 @@ +# La ligne de commande + +L'installation du paquet (`pip install sil-lift`) installe également la commande `sil-lift` — un outil pris en charge, dans l'esprit de LiftTools, fourni avec le paquet (et, pour `validate`, un exemple concret d'utilisation de l'API de la bibliothèque). + +``` +sil-lift validate PATH [--format {text,json}] [--strict] [--no-check-media] [--require-ids] + tous les problèmes, avec fichier/entrée/ligne ; sortie 1 en cas d’erreurs +sil-lift stats CHEMIN [--format {text,json}] + nombre d’entrées/sens/langues (en continu ; taille indifférente) +sil-lift sort CHEMIN [-o SORTIE] copie triée de manière canonique, prête pour la comparaison (par défaut : sur place) +sil-lift check-media PATH rapport sur les médias manquants et orphelins ; sortie 1 en cas de médias manquants +sil-lift export PATH [-o OUT] [--langs L] [--tsv] + une ligne par sens de feuille (sous-sens aplatis) vers CSV/TSV (en continu) +``` + +`--format json` écrit un seul objet JSON sur la sortie standard (et rien d'autre) à l'intention des outils d'intégration continue (CI) et d'automatisation ; voir le schéma dans l'exemple ci-dessous. L'option `--strict` traite les avertissements comme des erreurs et renvoie la valeur 1 si elle en détecte — utilisez-la pour conditionner la réussite d'une compilation à l'absence totale d'avertissements, plutôt qu'aux seules erreurs. L'option `--no-check-media` permet de ne pas effectuer la vérification de la présence des fichiers multimédias dans le système de fichiers (ce qui supprime les messages d'erreur de type `missing-media`), ce qui est utile lors de la validation d'une exportation fraîchement générée dont les fichiers audio et photos se trouvent ailleurs que dans le même dossier. `--require-ids` génère également une erreur (`missing-id`) pour toute entrée ne disposant pas d'un `guid` ou pour tout sens ne disposant pas d'un `id` — cette règle est plus stricte que celle de LIFT, pour les flux de travail qui réimportent à l'aide d'un identifiant stable. En passant `-` comme chemin d'accès, le document est lu à partir de l'entrée standard (un document transmis par canalisation n'ayant pas de dossier, son fichier associé `.lift-ranges` et ses médias ne sont pas résolus). La commande `stats` accepte également l'option `--format json`, qui renvoie les statistiques sous la forme d'un objet JSON unique. + +!!! note + Les codes de sortie de `validate` et le schéma `--format json` constituent une interface d'automatisation prise en charge : ils font tous deux l'objet de tests et ne sont modifiés que dans le respect de la norme SemVer. + +La commande `sort` ne modifie que le fichier `.lift` ; les fichiers `.lift-ranges` associés restent inchangés +(triez-les séparément à l'aide de l'API `RangesFile`). + +Les commandes `validate`, `stats`, `check-media` et `export` acceptent également un paquet LIFT compressé (un fichier `.zip` dans l'une ou l'autre des structures suivantes : fichiers à la racine de l'archive ou imbriqués dans un dossier de niveau supérieur) ; celui-ci est extrait dans un répertoire temporaire puis supprimé une fois la commande terminée. Les commandes de streaming `stats` et `export` n'extraient que le fichier `.lift` lui-même, ce qui leur permet de rester peu gourmandes en ressources pour les paquets contenant beaucoup de données multimédia ; les commandes `validate` et `check-media` ont besoin du dossier entier et l'extraient dans son intégralité. + +Exemples : + +``` +$ sil-lift validate dictionary.lift +erreur [dangling-ref] dictionary.lift:88 (entrée apu) : la référence « nope » ne correspond à aucun ID/GUID d'entrée ni à aucun ID de sens +avertissement [uri-not-rfc] dictionary.lift:6 : : lettre de lecteur Windows utilisée comme autorité URI (style FLEx file://C:/) +1 erreur(s), 1 avertissement(s) + +$ sil-lift validate dictionary.lift --format json +{ + "problems": [ + { + "level": "error", + "code": "dangling-ref", + "message": "la référence 'nope' ne correspond à aucun ID d'entrée/GUID ni à aucun ID de sens", + "file": "dictionary.lift", + "entry_id" : "apu", + "guid" : null, + "line" : 88 + }, + { + "level" : "warning", + "code" : "uri-not-rfc", + "message" : ": Lettre de lecteur Windows utilisée comme autorité URI (style FLEx file://C:/)", + "file": "dictionary.lift", + "entry_id": null, + "guid": null, + "line": 6 + } + ], + « summary » : { + « errors » : 1, + « warnings » : 1 + } +} + +$ sil-lift stats sango.lift +entrées : 3507 +sens : 4541 +... + +$ sil-lift export dictionary.lift --langs en,fr -o dictionary.csv +``` + +Codes de sortie : `0` : opération réussie (avertissements autorisés, sauf si l'option `--strict` est activée), `1` : problèmes détectés (erreurs de validation / fichiers multimédias manquants / avertissements lorsque l'option `--strict` est activée), `2` : données d'entrée illisibles. diff --git a/docs/fr/guides/folder-media.md b/docs/fr/guides/folder-media.md new file mode 100644 index 0000000..37711a5 --- /dev/null +++ b/docs/fr/guides/folder-media.md @@ -0,0 +1,41 @@ +# Le dossier LIFT : gammes et supports + +Un lexique LIFT se présente généralement sous la forme d'un _dossier_ : le fichier `.lift`, un ou plusieurs fichiers associés `.lift-ranges` (fichiers « sidecar ») et les fichiers multimédias contenus dans les dossiers `audio/` et `pictures/`. + +## Gammes + +```python +lex = sil_lift.load("dictionary.lift") # compagnons suivis automatiquement + +lex.ranges_files # {Path(...): RangesFile} +lex.all_ranges() # vue fusionnée {id: Range} +lex.all_ranges()["grammatical-info"].elements +``` + +La fonctionnalité « Companion Discovery » gère le monde réel : un `range/@href` pointant vers un fichier existant est utilisé ; les liens absolus « file://C:/... » orphelins de FieldWorks se rabattent sur le nom de base du lien situé à côté du fichier `.lift` ; et le fichier frère conventionnel `.lift-ranges` est pris en compte même lorsqu'aucun autre élément ne le référence. + +La fonction `lex.save()` enregistre le fichier `.lift` ainsi que tous les fichiers compagnons suivis. Les modifications apportées à un fichier `RangesFile` sont réenregistrées dans _ce_ fichier ; les plages non modifiées conservent exactement les mêmes octets. Utilisation autonome : + +```python +ranges = sil_lift.RangesFile.load("dictionary.lift-ranges") +ranges.find("grammatical-info") +ranges.sort() +ranges.save() +``` + +Transmettez `resolve_ranges=False` à la fonction `load()` pour ignorer la recherche de composants associés. + +## Médias + +```python +for ref in lex.media_refs(): # toutes les références de type « » et « + » print(ref.kind, ref.href, ref.entry_id) + +lex.missing_media() # références dont les fichiers n'existent pas +``` + +La résolution suit le schéma classique : un lien « href » relatif est vérifié tel qu'il est fourni (barres obliques inversées normalisées — WeSay écrit « pictures\photo with space.png ») et se trouve dans le répertoire « audio/ » (pour les fichiers audio de prononciation) ou « pictures/ » (pour les illustrations). Les liens « href » distants ou absolus ne peuvent pas être vérifiés et sont ignorés. + +## Autres éléments du dossier + +Un dossier LIFT contient souvent des fichiers que sil-lift ne modélise pas — le système d’écriture LDML dans `WritingSystems/`, les fichiers audio/image relatifs au consentement des locuteurs de The Combine dans `consent/`, etc. ; Les fonctions `load()` et `save()` ne les modifient pas, et [`Lexicon.save_zip()`](lift-export-interop.md) les transfère tels quels lors de la compression du dossier. diff --git a/docs/fr/guides/large-files.md b/docs/fr/guides/large-files.md new file mode 100644 index 0000000..f0da8a6 --- /dev/null +++ b/docs/fr/guides/large-files.md @@ -0,0 +1,27 @@ +# Fichiers volumineux (diffusion en continu) + +La fonction `load()` construit l'ensemble du graphe d'objets. Pour les lexiques de plusieurs centaines de Mo, l'API de traitement en continu traite les entrées une par une dans une mémoire limitée — il s'agit du même type `Entry` ; ainsi, le code écrit pour un mode fonctionne également dans l'autre. + +```python +import sil_lift + +with sil_lift.open_reader("big.lift") as reader: + header = reader.header # analysé au début (précède les entrées) + for entry in reader: # itérateur paresseux de type Iterator[Entry] + ... +``` + +```python +avec sil_lift.open_reader("big.lift") en tant que lecteur, sil_lift.open_writer( + "out.lift", header=reader.header, producer="my-script" +) en tant qu'écrivain : + pour chaque entrée dans le lecteur : + if not entry.date_deleted: # par ex. supprimer les entrées obsolètes + writer.write(entry) +``` + +Remarques : + +- Le résultat généré par l'éditeur correspond exactement à ce que produirait le sérialiseur canonique « plein document » pour le même contenu — les deux modes ne divergent jamais. +- Le mode streaming ne réutilise aucun octet source : la sortie est toujours canonique. Les résidus LIFT de niveau racine — commentaires entre les entrées et attributs hors schéma sur `` — ne sont pas transférés ; les entrées et l'en-tête sont complets, résidus compris. +- Si une exception est levée dans le corps d'un bloc `open_writer`, le fichier reste visiblement inachevé (pas de commande de fermeture ``) — un lexique à moitié écrit ne doit pas donner l'impression d'être complet. diff --git a/docs/fr/guides/lift-export-interop.md b/docs/fr/guides/lift-export-interop.md new file mode 100644 index 0000000..2ccc51d --- /dev/null +++ b/docs/fr/guides/lift-export-interop.md @@ -0,0 +1,112 @@ +# Création d'un fichier LIFT conforme + +Ce guide s'adresse à toute personne développant un _exportateur_ LIFT — c'est-à-dire un programme, quel que soit le langage utilisé, qui convertit le modèle de données d'une autre application au format LIFT 0.13. « sil-lift » remplit deux fonctions dans ce contexte : d'une part, il sert de filtre de conformité qui vérifie que la sortie respecte le schéma ainsi que les aspects sémantiques que ce dernier ne peut pas exprimer ; d'autre part, il sert de référence pour les formes et les règles de mise en forme du texte que la sortie doit respecter. + +Écrire du code LIFT est bien plus simple que de l'analyser : un exportateur ne génère que le sous-ensemble de constructions produit par son propre modèle, et n'est jamais confronté à l'ensemble des options prévues par la spécification complète. Le plus difficile, ce sont les détails — le compagnon `.lift-ranges`, le texte propre à chaque système d'écriture, les identifiants stables et l'échappement XML — et c'est précisément ce que détectent les vérifications ci-dessous. + +## Fichiers compressés + +LIFT est généralement transféré sous la forme d'un seul fichier `.zip` — FieldWorks et The Combine importent et exportent tous deux de cette manière — ; ainsi, `sil-lift` lit et écrit directement les paquets compressés, quelle que soit la structure utilisée par l'écosystème : les fichiers à la racine de l'archive ou imbriqués dans un dossier de premier niveau. + +- **À noter :** la commande `sil_lift.load("package.zip")` extrait le contenu dans un répertoire temporaire, localise le fichier `.lift` unique et le charge (les fichiers associés et les médias sont traités comme d'habitude). + - Les commandes CLI `validate`, `stats`, `check-media` et `export` acceptent également un chemin d'accès à un fichier `.zip` ; ainsi, le script ci-dessous s'exécute directement sur un paquet tel quel. + - les flux `stats` et `export`, et n'extraire que le fichier `.lift` plutôt que l'ensemble du paquet — afin que l'opération reste peu coûteuse sur un support contenant beaucoup de données, et que la limite d'extraction s'applique uniquement au fichier `.lift` et non à tout ce qui se trouve à côté. + - L'extraction est limitée à 10 GiB et à 100 000 éléments ; tout paquet dépassant l'une ou l'autre de ces limites est rejeté avec une erreur `LiftParseError`, tout comme ceux dont les chemins d'accès aux éléments sortent du répertoire d'extraction. +- **Écrivez :** `Lexicon.save_zip("out.zip", wrap_folder="MyDict")` regroupe le fichier `.lift`, ses fichiers `.lift-ranges` et tous les autres fichiers du dossier source (media, `WritingSystems/`, `consent/`, …) dans un fichier zip. + - `wrap_folder` utilise par défaut un dossier de niveau supérieur portant le nom du fichier zip (conformément à la convention d'importation de FieldWorks/Combine) ; passez `False` pour obtenir une archive plate. + +Les fichiers `.lift` et `.lift-ranges` conservent leur fidélité au niveau de l'octet au sein du paquet ; le conteneur zip lui-même n'est pas reproductible au niveau de l'octet. + +## Valider la sortie en tant que critère de conformité + +Passez `sil-lift validate` sur le fichier `.lift` généré. Il exécute RELAX NG (à la fois sur le fichier `.lift` et son fichier associé `.lift-ranges`) et effectue des vérifications sémantiques que la grammaire ne peut pas exprimer : références `relation`/`variant` orphelines, GUID en double, intégrité du parent des éléments de plage, valeurs de traits et d’informations grammaticales non définies dans leur plage, et références `range/@href` d’en-tête qui ne renvoient à aucun élément associé. + +Pour le CI, signaler tout échec et générer des résultats lisibles par machine : + +``` +sil-lift validate export.lift --strict --no-check-media --format json +``` + +- L'option `--strict` fait en sorte que les avertissements (et pas seulement les erreurs) entraînent l'échec de l'exécution. +- L'option `--no-check-media` permet d'ignorer la vérification de la présence des médias dans le système de fichiers, dont les résultats indiquant « `missing-media` » constituent un faux positif lorsque les fichiers audio/photo ne se trouvent pas dans le même dossier que le fichier `.lift` dans l'environnement CI. +- L'option `--format json` affiche un seul objet JSON (`{"problems": [...], "summary": {...}}`) au lieu d'un texte lisible par l'utilisateur ; ses codes de sortie et son schéma constituent une interface prise en charge et conforme à SemVer (voir [le guide de la ligne de commande](cli.md)). +- `--require-ids` génère également des erreurs lorsque des entrées ne comportent pas de `guid` ou détecte l'absence d'un `id` — ce qui s'avère utile lorsqu'une réimportation ultérieure doit mettre à jour les données existantes plutôt que de les dupliquer. + +Prévenez les pertes de données silencieuses (le type de défaillance qui entraîne des pertes lors d'une exportation CSV au format plat) en vérifiant les comptages à l'aide de la commande `stats --format json` sur votre modèle source : + +``` +sil-lift stats export.lift --format json +``` + +Il fournit le nombre d'« entrées », de « sens », d'« exemples », de « références multimédias », de « langues » et de « traits » par nom. + +### Exécuter le gate sans la chaîne d'outils Python + +Dans un projet TypeScript ou C#, l'environnement de CI peut effectuer la même vérification sans installer Python, grâce à l'action GitHub intégrée : + +```yaml +- uses : sillsdev/python-sil-lift@v0.1.0 + with : + path : export.lift + strict : "true" + no-check-media : "true" + format : json +``` + +ou l'image du conteneur, générée à partir du fichier `Dockerfile` du dépôt : + +``` +docker build -t sil-lift . +docker run --rm -v "$PWD:/work" -w /work sil-lift validate export.lift --strict +``` + +## Le compagnon `.lift-ranges` + +Les vocabulaires contrôlés — catégories grammaticales, domaines sémantiques et tout autre ensemble de valeurs associées à des caractéristiques — sont stockés dans un fichier `.lift-ranges` associé, référencé depuis le fichier `
` : + +```xml +
+ + + + +
+``` + +Le guide contient la définition complète de chaque gamme. Les valeurs sont des `` ; `parent` établit une hiérarchie ; `label` / `abbrev` / `description` sont des textes multiples : + +```xml + + + + + +
n
+
+
+ + + + + +
+``` + +Une entrée fait alors référence à une valeur par son identifiant : la catégorie grammaticale d'un sens est ``, et un domaine sémantique est ``. La commande `sil-lift validate` génère un avertissement (`undefined-range-value`) lorsqu'une valeur n'est pas définie dans son intervalle et une erreur (`range-parent`) lorsqu'un `parent` n'est pas un identifiant de frère — veillez donc à définir les intervalles réellement utilisés par vos données. Voir également [Gammes et supports](folder-media.md). + +Si vous créez l'exportation en Python, les méthodes `Lexicon.add_ranges_file()`, `RangesFile.add_range()` et `Range.add_element()` génèrent le fichier associé et ajoutent les références d'en-tête à votre place ; `open_writer(..., ranges=...)` effectue la même opération sur le chemin de streaming. + +## Texte et textes multiples + +Chaque chaîne de caractères en langue humaine dans LIFT est un _multitext_ : un `
` par système d'écriture, chacun contenant un `` : + +```xml + + kanga +
poulet
+
+``` + +Un modèle qui indexe les chaînes de caractères par code de langue (un `MultiString`, un `Record`, un `dict[str, str]`) s'adapte parfaitement à cette correspondance biunivoque : chaque entrée par clé correspond à un `
`. Un seul formulaire par langue est autorisé dans un même multitext — sinon, `sil-lift` affiche un avertissement `duplicate-form-lang`. + +L'échappement XML est le seul aspect qui nécessite une grande rigueur. Dans le texte d'un élément, les caractères `&`, `<`, and `>` doivent être échappés (`&`, `<`, `>`) ; dans les valeurs d'attribut, le caractère de guillemet doit également être échappé. Le programme « sil-lift » applique exactement ces règles et ne modifie jamais les espaces à l'intérieur de `` — il n'y ajoute aucune indentation, car cela altérerait les données lexicales. Si vous souhaitez obtenir le même résultat, réutilisez l'échappement d'un véritable sérialiseur XML (et non un remplacement fait maison qui oublie le caractère `&`) et conservez le contenu de `` octet par octet, tel qu'il apparaît dans votre source. diff --git a/docs/fr/guides/read-edit-write.md b/docs/fr/guides/read-edit-write.md new file mode 100644 index 0000000..693c0f7 --- /dev/null +++ b/docs/fr/guides/read-edit-write.md @@ -0,0 +1,66 @@ +# Lire, modifier, rédiger + +## Chargement en cours + +```python +import sil_lift + +lex = sil_lift.load("dictionary.lift") +``` + +La fonction `load()` accepte tout document LIFT **0.13** correctement formé, y compris les fichiers réels dont le schéma n'est pas valide. Tout ce qui n'est pas défini par le modèle (éléments/attributs inconnus, commentaires) est conservé sans perte sous forme de résidu LIFT dans le champ opaque `extra` de chaque nœud. Les autres versions de LIFT génèrent une exception `LiftParseError` indiquant le nom de la version. + +## Le modèle + +Chaque élément de LIFT est une classe de données typée : `Entry`, `Sense`, `Example`, `Pronunciation`, `Variant`, `Relation`, `Etymology`, `Reversal`, etc. Un texte multilingue est un `Multitext`, qui se comporte comme une correspondance entre un code de langue et un `Text` : + +```python +entry = lex.find(id="abat") + +str(entry.lexical_unit["seh"]) # "abat" +entry.lexical_unit["en"] = "grove" # les chaînes de caractères brutes sont converties +"en" in entry.citation # False +``` + +Le `Texte` est structuré — il s'agit d'une liste ordonnée de fragments `str` et `Span` — car `` peut contenir des balises `` imbriquées. `str(text)` convertit le contenu en texte brut ; les fragments conservent le balisage pour permettre la conversion aller-retour. + +Dans LIFT, les glosses ont la forme d’une _forme_ (chaque `` possède son propre langage) ; ainsi, un sens est défini par `glosses: list[Form]` et dispose d’une fonction d’aide : + +```python +sense = entry.senses[0] +sense.gloss("en") # Texte | None +entry.gloss_langs() # {"en", "id"} +``` + +## Enregistrement + +```python +lex.save() # retour à l'emplacement d'où il a été chargé +lex.save("elsewhere.lift") +``` + +Les entrées que vous n'avez pas modifiées sont réécrites **à l'octet près** ; un document que vous n'avez pas du tout modifié est identique à l'octet près, du premier au dernier octet. Pour consulter le contrat dans son intégralité, voir [Garanties Fidelity](../fidelity.md). + +## Construire à partir de zéro + +```python +lex = sil_lift.Lexicon(producer="my-script 1.0") +entry = sil_lift.Entry(id="hello", guid="...") +entry.lexical_unit["en"] = "hello" +sense = sil_lift.Sense() +sense.glosses.append(sil_lift.Form("fr", sil_lift.Text(["bonjour"]))) +entry.senses.append(sense) +lex.entries.append(entry) +lex.save("new.lift") +``` + +## Tri canonique + +```python +lex.sort() # entrées classées par (guid, id) ; plages/définitions de champs par id/balise +lex.save() # les entrées inchangées conservent exactement les mêmes octets, dans le nouvel ordre + +sil_lift.canonicalize("in.lift", "out.lift") # entièrement resérialisé, prêt pour la comparaison des différences +``` + +Voir aussi : [Exemple pratique : modification groupée des gloses](bulk-edit-glosses.md). diff --git a/docs/fr/guides/validate.md b/docs/fr/guides/validate.md new file mode 100644 index 0000000..6cd62ea --- /dev/null +++ b/docs/fr/guides/validate.md @@ -0,0 +1,36 @@ +# Valider + +La validation est toujours explicite : le chargement et l'enregistrement n'entraînent jamais de validation implicite. + +```python +import sil_lift + +# Approche exhaustive : un flux paresseux de problèmes (schéma + couches sémantiques). +for problem in sil_lift.iter_problems("dictionary.lift"): + print(problem) + # erreur [dangling-ref] dictionary.lift:88 (entrée apu) : la référence « nope » correspond à... + +# Détection rapide des erreurs : lève une exception LiftValidationError dès le premier problème de niveau d'erreur. +sil_lift.validate_file("dictionary.lift") + +# État en mémoire (sérialisation préalable — un coût documenté pour les grands lexiques) : +lex = sil_lift.load("dictionary.lift") +problems = list(lex.iter_problems()) +``` + +Chaque « problème » comporte un « niveau » (« erreur »/« avertissement »), un « code » fixe, un « message » et une adresse : « fichier », « id_entrée », « guid », « ligne ». + +## Les couches + +1. **RELAX NG** par rapport à la grammaire LIFT 0.13 (fournie par lift-standard — une copie identique au niveau des octets intégrée à ce paquet). +2. **Schéma Ranges** — le fichier `lift-ranges-0.13.rng` de ce projet — s'applique à tous les compagnons `.lift-ranges` suivis. +3. **Vérifications sémantiques** que la grammaire ne permet pas d'exprimer : `duplicate-guid`, `dangling-ref`, `range-parent`, `undefined-range-value`, `duplicate-form-lang`, `missing-media`. + +## Résultats concrets de FieldWorks (FLEx) + +FieldWorks génère systématiquement certains contenus qui sont rejetés par des outils de validation rigoureux. Voici la politique de sil-lift, afin que les véritables lexiques puissent être validés de manière utile : + +- Les liens `file://C:/...` (URI non valides) sont signalés comme des **avertissements** (`uri-not-rfc`), et non comme des erreurs de schéma — le validateur C# ne les a jamais rejetés. +- Les éléments enfants légalement entrelacés (par exemple, « champ, note, champ, note » en quelque sorte) ne sont **pas** signalés, ce qui permet de contourner un faux positif dans libxml2. +- Les valeurs de plage sont comparées selon la normalisation Unicode NFC — FLEx écrit le fichier `.lift` en NFC, mais le fichier `.lift-ranges` en NFD au sein de la même exportation. +- Les extensions `trait`/`field` de FLEx à l'intérieur de `range-element` **sont** signalées (erreurs de schéma par rapport au schéma des plages) : il s'agit de véritables écarts par rapport à la spécification. diff --git a/docs/fr/index.md b/docs/fr/index.md new file mode 100644 index 0000000..4bea817 --- /dev/null +++ b/docs/fr/index.md @@ -0,0 +1,32 @@ +# sil-lift + +Une bibliothèque Python pour [LIFT](https://github.com/sillsdev/lift-standard) (Lexicon Interchange FormaT) 0.13 : lecture/écriture sans perte du dossier LIFT (`.lift` + `.lift-ranges` + références multimédias), validation du schéma et de la sémantique, et tri canonique — avec des API de streaming pour les grands lexiques. + +**Statut : version préliminaire, en cours de développement.** + +## Installer + +Extrait de [PyPI](https://pypi.org/project/sil-lift/) : + +``` +pip install sil-lift # bibliothèque + la commande sil-lift +``` + +Nécessite Python 3.11 ou une version ultérieure. La seule dépendance d'exécution est lxml. + +## La visite guidée de 30 secondes + +```python +import sil_lift + +lex = sil_lift.load("thesaurus.lift") # suit également les entrées associées aux plages .lift + +for entry in lex.entries: + if "en" not in entry.gloss_langs(): + print(entry.id, str(entry.lexical_unit.get("seh") or "")) + +entry = lex.find(guid="0f5a9c3e-...") # ou lex.find(id="hoofd_a1b2") +entry.senses[0].definition["en"] = "head (anatomy)" + +lex.save() # les entrées non modifiées restent identiques au niveau des octets ; l'entrée modifiée est resérialisée +``` diff --git a/docs/fr/reference.md b/docs/fr/reference.md new file mode 100644 index 0000000..1964b24 --- /dev/null +++ b/docs/fr/reference.md @@ -0,0 +1,9 @@ +# Référence de l'API + +L'API publique correspond exactement à ce que `sil_lift` réexporte au niveau supérieur ; les modules préfixés par un trait de soulignement relèvent des détails d'implémentation. + +::: sil_lift +options: +show_root_heading: false +show_source: false +members_order: source diff --git a/docs/hi/csharp-differences.md b/docs/hi/csharp-differences.md new file mode 100644 index 0000000..6e7719e --- /dev/null +++ b/docs/hi/csharp-differences.md @@ -0,0 +1,52 @@ +# C# लाइब्रेरियों से अंतर + +sil-lift मोटे तौर पर SIL के C# LIFT टूलिंग के समान है — मुख्य रूप से [libpalaso](https://github.com/sillsdev/libpalaso) में `SIL.Lift` (पार्सर, वैलिडेटर, माइग्रेटर, `LiftSorter`), `SIL.DictionaryServices` उसी रिपॉजिटरी में (`LexEntry`/`LexSense` मॉडल, अपने स्वयं के LIFT रीडर/राइटर के साथ, जिसका उपयोग The Combine और WeSay करते हैं), और [Chorus](https://github.com/sillsdev/chorus) में LIFT हैंडलर। यह एक नया कार्यान्वयन है, पोर्ट नहीं। यह पृष्ठ उन स्थानों का सारांश प्रस्तुत करता है जहाँ व्यवहार जानबूझकर भिन्न होता है। + +## दायरा + +| क्षमता | सी# लाइब्रेरीज़ | सिल-लिफ्ट | +| ------------------- | ------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------- | +| लिफ्ट संस्करण | 0.10–0.13 (स्थानांतरण अंतर्निहित) | केवल **0.13**; पुराने संस्करणों को एक स्पष्ट त्रुटि के साथ अस्वीकार कर दिया जाता है। | +| संस्करण माइग्रेशन | `Migrator` (XSLT श्रृंखला) | कोई नहीं — एकमुश्त अपग्रेड के लिए लिफ्ट-स्टैंडर्ड में मौजूद XSLTs का उपयोग करें। | +| 3-तरफ़ा मर्ज / सिंक | कोरस | दायरे से बाहर | +| प्रमाणीकरण | केवल RELAX NG (`Validator`) | RELAX NG + रेंज स्कीमा + सेमांटिक जाँच | +| स्ट्रीमिंग | आंतरिक प्रविष्टि-कणिकीय पार्सिंग | सार्वजनिक `open_reader` / `open_writer` एपीआई | + +## एपीआई आकार + +`SIL.Lift` का पार्सर कॉलबैक-चालित (`ILexiconMerger`) है: यह पार्स इवेंट्स को एक कंज्यूमर पर पुश करता है। sil-lift इसके बजाय एक सादा ऑब्जेक्ट ग्राफ़ लौटाता है — प्रत्येक LIFT एलिमेंट के लिए टाइप्ड डेटाक्लासेस — क्योंकि पाइथन स्क्रिप्टर को कॉलबैक नहीं, ऑब्जेक्ट्स चाहिए। `SIL.DictionaryServices` `SIL.` पर एक `LexEntry`/`LexSense` ऑब्जेक्ट मॉडल की परत चढ़ाता है।Lift`पर एक`LexEntry`/`LexSense`ऑब्जेक्ट मॉडल की परत चढ़ाता है, लेकिन एक एप्लिकेशन मॉडल के रूप में यह केवल उन संरचनाओं का प्रतिनिधित्व करता है जिनका उपयोग वे एप्लिकेशन करते हैं — इसलिए इसके माध्यम से पुनः सीरियलाइज़ करने पर मॉडल के बाहर की सामग्री को sil-lift के LIFT अवशेष प्रबंधन और बाइट निष्ठा की तरह संरक्षित नहीं किया जा सकता (नीचे देखें)। स्ट्रीमिंग एपीआई वही`Entry\` प्रकार देता है, इसलिए सीखने के लिए कोई दूसरा, संक्षिप्त मॉडल नहीं है। + +## गमन-आगमन निष्ठा + +सबसे मजबूत जानबूझकर किया गया अंतर। `SIL.Lift` के साथ सेव करने पर पूरा दस्तावेज़ पुनः सीरियलाइज़ हो जाता है। सिल-लिफ्ट गारंटियाँ: + +- एक अपरिवर्तित दस्तावेज़ **बाइट-समान रूप से** सहेजता है, और +- अछूटी प्रविष्टियाँ अपने सटीक स्रोत बाइट्स को तब भी बनाए रखती हैं जब अन्य प्रविष्टियाँ बदलती हैं — वही प्रति-प्रविष्टि बाइट चंकिंग जो Chorus उपयोग करता है, स्वचालित रूप से लागू होती है। + +देखें [फिडेलिटी गारंटी](fidelity.md)। + +## प्रमाणीकरण + +C# `Validator` एक RELAX NG पास चलाता है और पहली त्रुटियों को स्ट्रिंग्स के रूप में रिपोर्ट करता है। sil-lift एक संरचित `Problem` स्ट्रीम रिपोर्ट करता है, जिनमें से प्रत्येक में संबंधित फ़ाइल, प्रविष्टि और पंक्ति होती है, और इसकी स्कीमा परत जानबूझकर तीन स्थानों पर विचलित होती है: + +- अमान्य URI चेतावनियाँ हैं, त्रुटियाँ नहीं। C# RELAX NG इंजन ने कभी भी `anyURI` डेटाटाइप को लागू नहीं किया, इसलिए FieldWorks (FLEx) वर्षों से वास्तविक शब्दकोशों में `file://C:/...` hrefs लिख रहा है। उन फ़ाइलों को अस्वीकार करने से लगभग हर FLEx एक्सपोर्ट पर निशान लग जाएगा। +- **Schematron नियम लागू किए जाते हैं** (सेमांटिक जांच के रूप में): LIFT व्याकरण में डुप्लिकेट फॉर्म भाषाओं और समान सह-बाधाओं को C# और कच्चे lxml सत्यापन दोनों द्वारा चुपचाप अनदेखा किया गया था। +- क्रॉस-फ़ाइल तुलनाएँ यूनिकोड-नॉर्मलाइज़्ड होती हैं, क्योंकि FLEx `.lift` को NFC में और साथी `.lift-ranges` को NFD में लिखता है। + +sil-lift एक लोडेड लेक्सिकन के `.lift-ranges` साथियों को स्टैंडअलोन रेंज दस्तावेज़ों के लिए एक स्कीमा के विरुद्ध भी मान्य करता है (जो बेस LIFT व्याकरण के साथ `lift-standard` से विकी हुई है) — हर ट्रैक की गई बाहरी रेंज फ़ाइल की जाँच तब की जाती है जब भी `.lift` का मान्यकरण किया जाता है — C# दुनिया में ऐसा कोई स्कीमा (या जाँच) नहीं है। (`.lift` से अलग, अकेले `.lift-ranges` फ़ाइल को मान्य करने का कोई एंट्री पॉइंट नहीं है।) + +## कैनोनिकल क्रमबद्धकरण + +`Lexicon.sort()` `LiftSorter` के मुख्य नियमों का पालन करता है (प्रविष्टियाँ केस-असंवेदनशील GUID द्वारा; रेंज और रेंज-तत्व ID द्वारा; हेडर फ़ील्ड परिभाषाएँ टैग द्वारा; अर्थ फ़ाइल क्रम में रखे जाते हैं; `` के अंदर की रिक्त स्थान को कभी भी छुआ नहीं जाता), तीन अंतरों के साथ: + +- guid के बिना प्रविष्टियाँ id के अनुसार क्रमबद्ध रूप से व्यवस्थित होती हैं (`LiftSorter` मानता है कि guid मौजूद है); +- ऑर्डरिंग लोकेल-स्वतंत्र है (साधारण केस-फोल्ड किए गए कोड पॉइंट्स, .NET इन्वेरिएंट-कल्चर कोलेशन नहीं); +- नोट्स, रिलेशन्स, और फॉर्म जैसी समान-प्रकार की सूचियाँ अपनी दस्तावेज़ व्यवस्था बनाए रखती हैं, बजाय इसके कि उन्हें कुंजी के अनुसार फिर से क्रमबद्ध किया जाए — समूहबद्धता पहले से ही निर्धारक है, और उन्हें पुनः क्रमबद्ध करने से केवल diff शोर बढ़ता है। + +spec रिपॉजिटरी का `canonicalizeLift.xsl` बिल्कुल भी उपयोग नहीं किया जाता है: यह लेक्सिकल टेक्स्ट के भीतर रिक्त स्थान को संकुचित कर देता है (विनाशकारी) और इसके उत्पन्न आईडी हर रन पर भिन्न होते हैं। + +## आगे नहीं बढ़ाया गया + +- WeSay-विशिष्ट सुविधाएँ (LIFT फ़ाइलों के संबंध में डैशबोर्ड/कॉन्फ़िग हैंडलिंग) +- `SynchronicMerger` (कोरस अपडेट मर्जिंग) — बाइट-चंकिंग का विचार फिडेलिटी लेयर में जीवित है, मर्जिंग नहीं। +- LDML लेखन-प्रणाली पार्सिंग: `WritingSystems/` में मौजूद फ़ाइलों को अपारदर्शी फ़ोल्डर सामग्री के रूप में माना जाता है। diff --git a/docs/hi/fidelity.md b/docs/hi/fidelity.md new file mode 100644 index 0000000..b001dcf --- /dev/null +++ b/docs/hi/fidelity.md @@ -0,0 +1,33 @@ +# फिडेलिटी गारंटी + +LIFT एक _इंटरचेंज_ प्रारूप है, इसलिए पहला नियम है **जो आप नहीं समझते उसे कभी न छोड़ें**। `sil-lift` का कॉन्ट्रैक्ट, जिसे हर रन पर टेस्ट सूट (कॉर्पस फ़ाइलें और प्रॉपर्टी-आधारित जेनरेशन) द्वारा सत्यापित किया जाता है: + +## पढ़ना + +कोई भी अच्छी तरह से निर्मित LIFT 0.13 दस्तावेज़ लोड हो जाता है — स्कीमा-अमान्य सामग्री सहित। जो कुछ भी मॉडल परिभाषित नहीं करता है, उसे निकटतम नोड के अपारदर्शी `Extras` कंटेनर में _LIFT अवशेष_ के रूप में रखा जाता है — FieldWorks द्वारा इसी विचार के लिए दिया गया नाम, जिसे यह `LiftResidue` फ़ील्ड में संग्रहीत करता है: अज्ञात गुण और तत्व, XML टिप्पणियाँ और प्रसंस्करण निर्देश, बिखरा हुआ पाठ, और दूषित प्रकारित गुण (एक खराब तारीख `Extras` में मूल स्ट्रिंग के रूप में बनी रहती है; टाइप्ड फ़ील्ड `None` होता है)। + +## बिना बदले दस्तावेज़ को सहेजें + +`load()` → `save()` बिना किसी संपादन के **बाइट-समान आउटपुट** लिखता है — कोई पुनः स्वरूपण नहीं, कोई पुनः एस्केपिंग नहीं, कोई पुनर्विन्यास नहीं, बाइट-ऑर्डर मार्क और XML घोषणाएँ शामिल हैं। वर्तमान में कोई सामान्यीकरण सूची नहीं है: पहचान सटीक है। + +अपवाद (लेखक पूर्ण कैनोनिकल सीरियलाइज़ेशन पर वापस चला जाता है, जो अर्थगत रूप से पूर्ण है लेकिन बाइट-संरक्षण नहीं करता): + +- स्रोत एन्कोडिंग ASCII-संगत नहीं है (UTF-8/US-ASCII नहीं), या +- स्रोत में एक DOCTYPE है, या +- बाइट स्कैनर और पार्सर दस्तावेज़ की शीर्ष-स्तरीय संरचना को लेकर असहमत हैं — उदाहरण के लिए विनिर्देश के बाहर का दूसरा `
`, जिसे पार्सर केवल एक बार रखता है (स्कैनर जानबूझकर सतर्क है: किसी भी संदेह पर यह कोई भी स्रोत बाइट नहीं पकड़ता), या +- स्रोत फ़ाइल से लोड करने के बजाय मेमोरी में बनाया गया था। + +## संपादित दस्तावेज़ सहेजें + +- **अछूटी प्रविष्टियाँ उनके मूल बाइट्स से शब्दशः उत्सर्जित की जाती हैं।** एक प्रविष्टि को तब छूई हुई माना जाता है यदि पार्स के बाद उसके मॉडल ऑब्जेक्ट का कोई भी भाग बदला गया हो (यह कैनोनिकल-सीरियलाइजेशन स्नैपशॉट द्वारा पता लगाया जाता है, डर्टी फ्लैग द्वारा नहीं)। +- स्पर्शित प्रविष्टियाँ मानक रूप से और पूरी तरह से पुनः सीरियलाइज़ की जाती हैं: UTF-8, मिश्रित सामग्री के बाहर 2-स्पेस इंडेंटेशन (`` और `` के अंदर रिक्त स्थान कभी नहीं बदला जाता), प्रत्येक तत्व के लिए एक प्रलेखित उपसमूह (उदाहरण के लिए प्रविष्टि: शब्द-इकाई, उद्धरण, उच्चारण, रूप, अर्थ, टिप्पणियाँ, संबंध, व्युत्पत्तियाँ, टिप्पणियाँ, विशेषताएँ, क्षेत्र), निश्चित गुणक्रम, ISO-8601 में दिनांक (`Z` UTC के लिए)। सभी अवशेष पुनः उत्सर्जित किए जाते हैं; इसकी स्थिति मूल चाइल्ड इंडेक्स पर पुनर्स्थापित की जाती है, और इसे नई चाइल्ड सूची से क्लैम्प किया जाता है (एक अनुमान — सटीक बाइट स्थितियाँ केवल अछूती प्रविष्टियों के लिए ही गारंटी की जाती हैं)। +- प्रविष्टियाँ जोड़ने, हटाने या पुनर्व्यवस्थित करने से दस्तावेज़ की संरचना पुनः अनुक्रमित होती है, लेकिन फिर भी हर अपरिवर्तित प्रविष्टि के बाइट्स को शब्दशः उत्सर्जित किया जाता है। + +!!! note ""यहाँ का कैनोनिकल" किसी अन्य कैनोनिकल XML से संबंधित नहीं है।" + इस पृष्ठ पर कैनोनिकल रूप का अर्थ है `sil-lift` का अपना प्रलेखित स्वरूप, जिसे ऊपर एक बुलेट में वर्णित किया गया है। यह W3C की कैनोनिकल XML (C14N) प्रक्रिया से असंबंधित है। यह `SIL.Core` की `CanonicalXmlSettings` क्लास से असंबंधित है। + +## ज्ञात अनुमान (केवल स्पर्श किए गए नोड्स) + +- \` रन के अंदर की टिप्पणियाँ संरक्षित रहती हैं, लेकिन उन्हें रन के ठीक कैरेक्टर ऑफ़सेट पर रखने के बजाय रन के बगल में स्थानांतरित कर दिया जाता है। +- संपादित तत्व के भीतर क्रॉस-टाइप बाल क्रम को मानक समूहबद्धता में सामान्यीकृत किया जाता है (LIFT स्कीमा का `interleave` इस क्रम को अर्थगत रूप से महत्वहीन बना देता है)। +- एक मल्टीटेक्स्ट तत्व जो मौजूद तो है लेकिन कुछ भी नहीं ले जाता — न कोई फॉर्म, न कोई अवशेष, उदाहरण के लिए `` — पुनः उत्सर्जित नहीं होता। मॉडल इन फ़ील्ड्स को हमेशा मौजूद `Multitext` (`lexical-unit`, `citation`, `definition`, किसी संबंध का `usage`, और `label` / `abbrev` / `description` url-refs, रेंज, रेंज-एलिमेंट्स और हेडर पर), इसलिए पार्सिंग के बाद एक खाली फ़ील्ड का अभाव वाले फ़ील्ड से कोई अंतर नहीं किया जा सकता। कोई भी अर्थपूर्ण बात खोई नहीं है। diff --git a/docs/hi/guides/build-export.md b/docs/hi/guides/build-export.md new file mode 100644 index 0000000..90dc1aa --- /dev/null +++ b/docs/hi/guides/build-export.md @@ -0,0 +1,168 @@ +# कार्य उदाहरण: शून्य से LIFT एक्सपोर्ट बनाना + +यदि आप किसी अन्य एप्लिकेशन का डेटा LIFT के रूप में निर्यात कर रहे हैं — [अनुरूप LIFT उत्पन्न करने](lift-export-interop.md) के पीछे का कार्य — तो `sil-lift` दस्तावेज़ के प्रत्येक ऑब्जेक्ट को क्रमबद्ध करके उसे सीरियलाइज़ कर सकता है, बजाय इसके कि आप XML को हाथ से उत्पन्न करें। यह एक स्क्रिप्ट के माध्यम से दिखाता है कि कैसे एक प्रविष्टि तैयार की जाती है, जिसमें एक वास्तविक शब्दकोश के सभी घटक होते हैं (कई लेखन प्रणालियाँ, उच्चारण, एक अर्थ उदाहरण के साथ, एक चित्रण, एक अर्थ-क्षेत्रीय गुण, और एक ऐप-विशिष्ट फ़ील्ड), नियंत्रित शब्दावलियों को `.lift-ranges` साथी फ़ाइल में लिखा जाता है, सत्यापित किया जाता है, और सहेजा जाता है। + +## पटकथा + +```python +from pathlib import Path + +import sil_lift + +lex = sil_lift.Lexicon(producer="my-exporter") + +# एक प्रविष्टि, स्रोत मॉडल से निर्मित। +entry = sil_lift.Entry(id="kanga", guid="6b9e7c2a-3f4d-4a1b-8c5e-2d9f0a1b2c3d") +entry.lexical_unit["seh"] = "nkhuku" +entry.lexical_unit["pt"] = "galinha" + +pron = sil_lift.Pronunciation() +pron.forms["en"] = "Speaker: Ana" # द कंबाइन का स्पीकर-लेबल कन्वेंशन +pron.media.append(sil_lift.URLRef(href="audio/nkhuku.wav")) +entry.pronunciations.append(pron) + +sense = sil_lift.Sense(id="kanga_s1") +sense.grammatical_info = sil_lift.GrammaticalInfo(value="संज्ञा") +sense.glosses.append(sil_lift.Form(lang="en", text=sil_lift.Text(["chicken"]))) +sense.definition["en"] = "a domestic fowl kept for its eggs and meat" + +example = sil_lift.Example() +example.forms["seh"] = "Ndinafuna nkhuku." +translation = sil_lift.Translation() +translation.forms["en"] = "मुझे एक मुर्गी चाहिए।" +example.translations.append(translation) +sense.examples.append(example) + +photo = sil_lift.URLRef(href="pictures/hen.jpg") +photo.label["en"] = "एक मुर्गी" +sense.illustrations.append(photo) + +sense.traits.append(sil_lift.Trait(name="semantic-domain-ddp4", value="1.6.1.2")) + +scientific = sil_lift.Field(type="scientific-name") # एक ऐप-विशिष्ट अतिरिक्त फ़ील्ड +scientific.content["en"] = "Gallus gallus domesticus" +sense.fields.append(scientific) + +entry.senses.append(sense) +lex.entries.append(entry) + +# एंट्री जिन नियंत्रित शब्दावलियों का संदर्भ देती है, एक साथी .lift-ranges में। +ranges = sil_lift.RangesFile() +ranges.add_range("grammatical-info").add_element("Noun").label["en"] = "noun" +ranges.add_range("semantic-domain-ddp4").add_element("1.6.1.2").label["en"] = "Bird" +lex.add_ranges_file(ranges, href="birds.lift-ranges") + +# डिस्क पर लिखने से पहले save() क्या लिखेगा, इसकी जांच करें। +problems = list(lex.iter_problems()) +print(f"validation: {len(problems)} problem(s)") + +out = Path("export") +out.mkdir(exist_ok=True) +lex.save(out / "birds.lift") +print("=== birds.lift ===") +print((out / "birds.lift").read_text(encoding="utf-8"), end="") +print("=== birds.lift-ranges ===") +print((out / "birds.lift-ranges").read_text(encoding="utf-8"), end="") +``` + +## यह क्या उत्पन्न करता है + +`validation: 0 problem(s)`, फिर `.lift` और उसका साथी एक साथ: + +``` +=== birds.lift === + + +
+ + + + +
+ + + + nkhuku + +
+ galinha +
+
+ +
+ Speaker: Ana +
+ +
+ + + + chicken + + +
+ a domestic fowl kept for its eggs and meat +
+
+ +
+ Ndinafuna nkhuku. +
+ +
+ I want a chicken. +
+
+
+ + + + + +
+ Gallus gallus domesticus +
+
+
+
+
+=== birds.lift-ranges === + + + + + + + + + + + + + +``` + +## एपीआई पर नोट्स + +- मल्टीटेक्स्ट फ़ील्ड (`lexical_unit`, `definition`, एक `Form`/`URLRef` लेबल, एक `Field` की सामग्री, ...) लेखन प्रणाली के लिए एक स्ट्रिंग मैपिंग इंटरफ़ेस के माध्यम से लें: `entry.lexical_unit["seh"] = "nkhuku"` एक `
` जोड़ता है। एक स्रोत मॉडल जो भाषा कोड के आधार पर स्ट्रिंग्स को मैप करता है, सीधे इस पर लागू होता है। +- `RangesFile.add_range()` / `Range.add_element()` नियंत्रित शब्दावलियाँ बनाती हैं, और `Lexicon.add_ranges_file(ranges, href=...)` साथी फ़ाइल को जोड़ती है और हेडर `` संदर्भ जोड़ती है — ताकि प्रविष्टि के `` और `` आपके द्वारा परिभाषित रेंज के अनुसार हल हों। +- `URLRef` एक href और एक वैकल्पिक कैप्शन/लेबल मल्टीटेक्स्ट है — जो `` (ऑडियो) और `` (फ़ोटो) दोनों के लिए उपयोग किया जाता है। यहाँ का उच्चारण द कॉम्बाइन की परंपरा का पालन करता है, जिसमें `en` रूप को `Speaker: ` के रूप में पढ़ा जाता है। +- ऐप-विशिष्ट डेटा जिसमें कोई नेटिव LIFT होम राइड्स नहीं होतीं, एक `` (या ``) के रूप में: FieldWorks इन्हें कस्टम फ़ील्ड्स के रूप में पढ़ता है और The Combine इन्हें संरक्षित करता है। +- प्रत्येक प्रविष्टि को एक वास्तविक, स्थिर `guid` दें (उदाहरण के लिए `uuid.uuid4()` से, जो एक्सपोर्ट्स में पुन: उपयोग किया जाता है) — बाद में पुनः आयात करने पर यह प्रविष्टि को डुप्लिकेट करने के बजाय उसी स्थान पर अपडेट कर देता है। `sil-lift validate --require-ids` इसे लागू करता है। +- `lex.iter_problems()` मेमोरी में मौजूद दस्तावेज़ (जिसे `save()` लिखता) को डिस्क पर लिखने से पहले सत्यापित करता है; यहाँ यह सही है। चूंकि लेक्सिकॉन में अभी तक कोई फ़ोल्डर नहीं है, इसलिए मीडिया-उपस्थिति और साथी-href जांच छोड़ दी जाती हैं — ऑडियो और फोटो फ़ाइलें तैयार होने पर सहेजे गए आउटपुट पर [`sil-lift validate`](cli.md) चलाएँ (या `--no-check-media` के साथ)। + +## पैकेजिंग + +`lex.save("export/birds.lift")` फ़ोल्डर फ़ॉर्म (`.lift` + `.lift-ranges` एक साथ) लिखता है। FieldWorks और The Combine द्वारा सीधे आयात किए जाने वाले एकल ज़िप पैकेज को उत्पन्न करने के लिए, इसके बजाय `lex.save_zip("birds.zip")` का उपयोग करें — देखें [अनुरूप LIFT का उत्पादन](lift-export-interop.md)। diff --git a/docs/hi/guides/bulk-edit-glosses.md b/docs/hi/guides/bulk-edit-glosses.md new file mode 100644 index 0000000..1b7dcee --- /dev/null +++ b/docs/hi/guides/bulk-edit-glosses.md @@ -0,0 +1,79 @@ +# कार्य किया गया उदाहरण: ग्लॉस का थोक-संपादन + +एक सामान्य रखरखाव कार्य: शब्दकोश में प्रत्येक अंग्रेज़ी ग्लॉस में वर्तनी को सामान्य करना (ब्रिटिश → अमेरिकी, या इसके विपरीत), बिना फ़ाइल में किसी अन्य चीज़ को प्रभावित किए। यह एक स्क्रिप्ट के माध्यम से लोड, संपादन, सत्यापन और सहेजने की प्रक्रिया को दिखाता है — संपादन API और निष्ठा गारंटी को एक साथ काम करते हुए। + +## पटकथा + +```python +import sys + +import sil_lift + +path = "dictionary.lift" +lex = sil_lift.load(path) + + +def iter_senses(senses): + """प्रत्येक अर्थ, उप-अर्थों सहित (पुनरावर्ती) उत्पन्न करें।""" + for sense in senses: + yield sense + yield from iter_senses(sense.subsenses) + + +edited_glosses = 0 + +for entry in lex.entries: + for sense in iter_senses(entry.senses): + for gloss in sense.glosses: + if gloss.lang != "en": + continue + old = str(gloss.text) + new = old.replace("colour", "color") + if new != old: + gloss.text = sil_lift.Text([new]) + edited_glosses += 1 + +changed = lex.changed_entries() + +errors = [p for p in lex.iter_problems() if p.level == "error"] +if errors: + for problem in errors: + print(problem) + sys.exit(f"aborting: {len(errors)} validation error(s), nothing saved") + +lex.save() +print(f"edited {edited_glosses} gloss(es) across {len(changed)} entry(ies)") +``` + +ध्यान देने योग्य कुछ बातें: + +- `Sense.subsenses` स्वयं एक `list[Sense]` है, इसलिए `iter_senses` इसमें पुनरावृत्ति करता है — एक सामूहिक संपादन जो केवल `entry.senses` को ही चलाता है, वह किसी उपसंज्ञा के अंतर्गत निहित किसी भी परिभाषा को चुपचाप छोड़ देगा। +- `gloss.text` एक `Text` है, न कि एक साधारण स्ट्रिंग: `str(gloss.text)` इसे मिलान के लिए फ्लैटन करता है, और प्रतिस्थापन को स्ट्रिंग को वहीं पर बदलने के बजाय `sil_lift.Text([new])` के साथ वापस लिखा जाता है। +- `lex.changed_entries()` रिपोर्ट करता है कि लोड की गई फ़ाइल से कौन-सी प्रविष्टियाँ भिन्न हैं। चूंकि एक प्रविष्टि का डाइजेस्ट उसके पूरे सबट्री को कवर करता है, एक नेस्टेड सबसेंस में संपादन उस प्रविष्टि की रिपोर्ट करता है जिसमें वह शामिल है। + - यह सीरियलाइज़्ड सामग्री की तुलना करता है, इसलिए किसी फ़ील्ड को उसका पूर्व मान वापस असाइन करने पर यह रिपोर्ट नहीं होता। + - यह केवल सामग्री में हुए परिवर्तनों की रिपोर्ट करता है; `lex.added_entries()` और `lex.removed_entries()` उन प्रविष्टियों को कवर करते हैं जो लोड होने के बाद से दिखाई दीं या गायब हो गईं। + - यह प्रविष्टियों को स्वयं लौटाता है, चाहे `id` दोहराया गया हो या अनुपस्थित हो (जिसकी अनुमति LIFT देता है)। + - एक गणना के रूप में, यह केवल तभी सार्थक होती है जब तुलना करने के लिए कुछ मौजूद हो। जब बाइट स्कैनर स्रोत पढ़ने से इनकार करता है — ऐसी एन्कोडिंग जो ASCII-संगत नहीं है, या स्कैनर/पार्सर के बीच असहमति — तब कोई आधार रेखा नहीं होती, और `changed_entries()` हर प्रविष्टि की रिपोर्ट करता है। WriteGuard के लिए यह ईमानदार उत्तर है, क्योंकि `save()` उस स्थिति में पूरी फ़ाइल को फिर से सीरियलाइज़ करता है, लेकिन इसका मतलब है कि काउंट संपादन के आकार के बजाय शब्दकोश के आकार के बराबर होता है। +- `lex.changes()` यह रिपोर्ट करता है कि दस्तावेज़ में _बिल्कुल भी_ बदलाव हुआ है या नहीं। यह केवल प्रविष्टियों को ही नहीं, बल्कि हेडर, रूट एलिमेंट और प्रत्येक `.lift-ranges` साथी को भी कवर करता है। + - यह केवल तब फर्सी होता है जब `save()` स्रोत बाइट्स को पुन: उत्पन्न कर सके, जो `if not lex.changes(): ...` को अनावश्यक लेखन छोड़ने का सही तरीका बनाता है। गारंटी एकतरफा है: यह कभी भी उस दस्तावेज़ के लिए "लिखने के लिए कुछ नहीं" रिपोर्ट नहीं करती जिसे फिर से लिखा जाएगा, जबकि एक ऐसा परिवर्तन जो पूरी पुनः-सीरियलाइज़ेशन को मजबूर करता है, मूल बाइट्स पर वापस आ सकता है और फिर भी रिपोर्ट किया जा सकता है। + - यह सामग्री की तुलना करता है, गंतव्य की नहीं, इसलिए इसके साथ केवल उसी स्थान पर सहेजें: `lex.save(some_other_dir / "dictionary.lift")` दस्तावेज़ और उसके साथियों को एक ऐसी जगह पर लिखता है जहाँ अभी तक कुछ भी नहीं है, चाहे कुछ बदला हो या नहीं। + - यह एक गार्ड है, स्पीड-अप नहीं — इसे कॉल करने पर यह हर एंट्री को प्रोसेस करता है, जो वही काम है जो `save()` यह तय करने के लिए करता है कि कौन से सोर्स बाइट्स को वह फिर से इस्तेमाल कर सकता है, इसलिए आप जो स्किप करते हैं वह निर्णय लेने का प्रयास नहीं, बल्कि स्वयं लेखन है (फाइल-संशोधन का समय अपरिवर्तित रहता है, कोई झूठा diff नहीं)। +- मेमोरी में सत्यापन (`lex.iter_problems()`) पहले संपादित स्थिति को सीरियलाइज़ करता है, ताकि डिस्क पर कुछ भी लिखे जाने से पहले यह संपादन को सही ढंग से दर्शाए। किसी भी `"error"`-स्तर के `Problem` पर रद्द करने का मतलब है — चेतावनियाँ कॉल करने वाले पर छोड़ दी जाती हैं ताकि वह निर्णय ले सके — कि एक खराब संपादन कभी भी `save()` तक नहीं पहुँचता। + +इस तरह छूने लायक सिर्फ ग्लॉस ही नहीं हैं। एक ही `Multitext` मैपिंग सतह परिभाषाओं और किसी प्रविष्टि या अर्थ पर प्रत्येक अन्य बहुभाषी क्षेत्र पर लागू होती है: + +```python +sense.definition["en"] = "किसी वस्तु का रंग" +``` + +## इसे चलाना + +एक छोटे शब्दकोश के खिलाफ चलाएँ, जिसमें एक परिभाषा और एक उप-अर्थ परिभाषा दोनों में "रंग" लिखा हो: + +``` +1 प्रविष्टि(यों) में 2 ग्लॉस(स) संपादित +``` + +## वफ़ादारी का इनाम + +यह गारंटी प्रत्येक प्रविष्टि के लिए है: जिस प्रविष्टि का मॉडल नहीं बदला, वह बिल्कुल उसी रूप में वापस आती है जैसा पढ़ी गई थी, और केवल वे प्रविष्टियाँ जो वास्तव में बदली गई थीं, पुनः सीरियलाइज़ की जाती हैं। उपरोक्त रन में, एक प्रविष्टि के ग्लॉस संपादित किए गए थे — फ़ाइल में बाकी सभी प्रविष्टियों ने अपने सटीक बाइट्स बनाए रखे। (बारीकी पर ध्यान दें: प्रविष्टि के किसी भी भाग को संपादित करने पर वह पूरी प्रविष्टि पुनः अनुक्रमित हो जाती है, जिसमें उसके अप्रभावित सहोदर अर्थ भी शामिल हैं।) इसलिए 50,000 प्रविष्टियों वाले शब्दकोश में एक ग्लॉस का संपादन एक प्रविष्टि को प्रभावित करने वाला diff उत्पन्न करता है, न कि एक पुनः स्वरूपित फ़ाइल। सटीक अनुबंध के लिए [फिडेलिटी गारंटी](../fidelity.md) देखें। diff --git a/docs/hi/guides/cli.md b/docs/hi/guides/cli.md new file mode 100644 index 0000000..52bee0a --- /dev/null +++ b/docs/hi/guides/cli.md @@ -0,0 +1,69 @@ +# कमांड लाइन + +पैकेज (`pip install sil-lift`) इंस्टॉल करने पर `sil-lift` कमांड भी इंस्टॉल हो जाता है — यह LiftTools की भावना में एक समर्थित टूल है जो पैकेज के साथ आता है (और `validate` के लिए लाइब्रेरी API का एक कार्यशील उदाहरण)। + +``` +sil-lift validate PATH [--format {text,json}] [--strict] [--no-check-media] [--require-ids] + सभी समस्याएँ, फ़ाइल/प्रविष्टि/पंक्ति के साथ; त्रुटियों पर 1 पर बाहर निकलें +sil-lift stats PATH [--format {text,json}] + प्रविष्टि/अर्थ/भाषा की गिनती (स्ट्रीमिंग; कोई भी आकार) +sil-lift sort PATH [-o OUT] मानकीकृत रूप से क्रमबद्ध, diff-तैयार प्रतिलिपि (डिफ़ॉल्ट: उसी स्थान पर) +sil-lift check-media PATH अनुपस्थित और अनाथ मीडिया रिपोर्ट; यदि अनुपस्थित हो तो 1 पर निकास +sil-lift export PATH [-o OUT] [--langs L] [--tsv] + प्रत्येक लीफ़ सेंस के लिए एक पंक्ति (सबसेंस समतल) CSV/TSV में (स्ट्रीमिंग) +``` + +`--format json` CI/ऑटोमेशन के उपयोग के लिए stdout पर एक ही JSON ऑब्जेक्ट लिखता है (और कुछ नहीं); नीचे दिए गए उदाहरण में स्कीमा देखें। `--strict` चेतावनियों को त्रुटियों के रूप में मानता है, और कोई भी चेतावनी मिलने पर 1 पर बाहर निकल जाता है — इसका उपयोग बिल्ड को केवल त्रुटियों पर नहीं बल्कि बिल्कुल भी चेतावनी न होने पर रोकने के लिए करें। `--no-check-media` फ़ाइल सिस्टम मीडिया-उपस्थिति जाँच को छोड़ देता है (`missing-media` निष्कर्षों को दबाते हुए), जो तब उपयोगी होता है जब किसी ताज़ा उत्पन्न निर्यात का सत्यापन किया जा रहा हो, जिसकी ऑडियो/फ़ोटो फ़ाइलें उसी फ़ोल्डर में होने के बजाय कहीं और स्थित हों। `--require-ids` अतिरिक्त रूप से किसी भी ऐसी प्रविष्टि पर विफल होता है जिसमें `guid` न हो या किसी सेंस में `id` न हो — यह LIFT की तुलना में अधिक सख्त है, उन वर्कफ़्लो के लिए जो एक स्थिर id द्वारा पुनः आयात करते हैं। पथ के रूप में `-` पास करने पर दस्तावेज़ stdin से पढ़ा जाता है (एक पाइप्ड दस्तावेज़ का कोई फ़ोल्डर नहीं होता, इसलिए इसका साथी `.lift-ranges` और मीडिया हल नहीं होते)। `stats` भी `--format json` लेता है, और गिनतियों को एक ही JSON ऑब्जेक्ट के रूप में उत्पन्न करता है। + +!!! note + `validate` के एग्जिट कोड और `--format json` स्कीमा एक समर्थित ऑटोमेशन इंटरफ़ेस हैं: दोनों टेस्ट द्वारा कवर किए गए हैं और केवल SemVer के तहत ही बदलते हैं। + +`sort` केवल `.lift` फ़ाइल को ही पुनः लिखता है; साथी `.lift-ranges` फ़ाइलें अछूती रह जाती हैं (उन्हें `RangesFile` API के साथ अलग से क्रमबद्ध करें)। + +`validate`, `stats`, `check-media`, और `export` एक ज़िप किए गए LIFT पैकेज को भी स्वीकार करते हैं (दोनों लेआउट में एक `.zip` — आर्काइव रूट में फ़ाइलें, या एक टॉप-लेवल फ़ोल्डर के अंदर); इसे एक अस्थायी निर्देशिका में निकाला जाता है और कमांड समाप्त होने पर हटा दिया जाता है। स्ट्रीमिंग कमांड्स `stats` और `export` केवल `.lift` को ही एक्सट्रेक्ट करते हैं, इसलिए ये मीडिया-भारी पैकेजों पर सस्ते पड़ते हैं; `validate` और `check-media` को पूरे फ़ोल्डर की ज़रूरत होती है और वे सब कुछ एक्सट्रेक्ट करते हैं। + +उदाहरण: + +``` +$ sil-lift validate dictionary.lift +त्रुटि [dangling-ref] dictionary.lift:88 (entry apu): ref 'nope' किसी भी एंट्री आईडी/GUID या सेंस आईडी से मेल नहीं खाता +चेतावनी [uri-not-rfc] dictionary.lift:6: : URI प्राधिकरण के रूप में Windows ड्राइव अक्षर का उपयोग (FLEx-शैली file://C:/) +1 त्रुटि(एँ), 1 चेतावनी(एँ) + +$ sil-lift validate dictionary.lift --format json +{ + "problems": [ + { + "level": "error", + "code": "dangling-ref", + "message": "ref 'nope' matches no entry id/guid or sense id", + "file": "dictionary.lift", + "entry_id": "apu", + "guid": null, + "line": 88 + }, + { + "level": "warning", + "code": "uri-not-rfc", + "message": ": Windows ड्राइव अक्षर को URI प्राधिकरण के रूप में उपयोग किया गया (FLEx-शैली file://C:/)", + "file": "dictionary.lift", + "entry_id": null, + "guid": null, + "line": 6 + } + ], + "summary": { + "errors": 1, + "warnings": 1 + } +} + +$ sil-lift stats sango.lift +entries: 3507 +senses: 4541 +... + +$ sil-lift export dictionary.lift --langs en,fr -o dictionary.csv +``` + +एग्जिट कोड: `0` सफलता (चेतावनियाँ अनुमत हैं, जब तक `--strict` न हो), `1` निष्कर्ष (मान्यकरण त्रुटियाँ / मीडिया अनुपस्थित / `--strict` के तहत चेतावनियाँ), `2` अपठनीय इनपुट। diff --git a/docs/hi/guides/folder-media.md b/docs/hi/guides/folder-media.md new file mode 100644 index 0000000..af9b40f --- /dev/null +++ b/docs/hi/guides/folder-media.md @@ -0,0 +1,41 @@ +# LIFT फ़ोल्डर: श्रेणियाँ और मीडिया + +एक LIFT शब्दकोश आमतौर पर एक _फ़ोल्डर_ होता है: `.lift` फ़ाइल, एक या अधिक `.lift-ranges` साथी (साइडकार फ़ाइलें), और `audio/` / `pictures/` मीडिया। + +## दायरे + +```python + +lex = sil_lift.load("dictionary.lift") # साथी स्वचालित रूप से ट्रैक किए जाते हैं + +lex.ranges_files # {Path(...): RangesFile} view +lex.all_ranges()["grammatical-info"].elements +``` + +साथी खोज वास्तविक दुनिया को संभालती है: एक `range/@href` जिसका संदर्भ किसी मौजूदा फ़ाइल की ओर है, का उपयोग किया जाता है; FieldWorks के dangling absolute `file://C:/...` hrefs `.lift` के पास होने पर href के basename पर वापस आ जाते हैं; और पारंपरिक `.lift-ranges` sibling तब भी लिया जाता है जब कोई भी इसे संदर्भित नहीं करता। + +`lex.save()` `.lift` और प्रत्येक ट्रैक किए गए साथी को एक साथ लिखता है। `RangesFile` में किए गए संपादन _उसकी_ फ़ाइल में वापस सहेजे जाते हैं; बिना बदले रेंज अपने सटीक बाइट्स बनाए रखते हैं। स्वतंत्र उपयोग: + +```python +ranges = sil_lift.RangesFile.load("dictionary.lift-ranges") +ranges.find("grammatical-info") +ranges.sort() +ranges.save() +``` + +साथी खोज को छोड़ने के लिए `load()` को `resolve_ranges=False` पास करें। + +## माध्यम + +```python +for ref in lex.media_refs(): # हर और + print(ref.kind, ref.href, ref.entry_id) + +lex.missing_media() # उन रेफ़रेंसों के लिए जिनकी फाइलें मौजूद नहीं हैं +``` + +रिज़ॉल्यूशन पारंपरिक लेआउट का अनुसरण करता है: एक सापेक्ष href को दिए गए रूप में जांचा जाता है (बैकस्लैश को सामान्यीकृत किया गया — WeSay लिखता है `pictures\photo with space.png`) और `audio/` (उच्चारण मीडिया के लिए) या `pictures/` (चित्रण के लिए) के अंतर्गत। रिमोट/एब्सोल्यूट hrefs की जाँच नहीं की जा सकती और उन्हें छोड़ दिया जाता है। + +## अन्य फ़ोल्डर की सामग्री + +एक LIFT फ़ोल्डर में अक्सर ऐसी फ़ाइलें होती हैं जिन्हें sil-lift मॉडल नहीं करता — जैसे `WritingSystems/` के अंतर्गत लेखन-प्रणाली LDML, `consent/` के अंतर्गत The Combine के वक्ता सहमति ऑडियो/छवि फ़ाइलें, और इसी तरह की अन्य फ़ाइलें; `load()`/`save()` इन्हें बिना छुए छोड़ देते हैं, और [`Lexicon.save_zip()`](lift-export-interop.md) फ़ोल्डर को पैकेज करते समय इन्हें शब्दशः शामिल करता है। diff --git a/docs/hi/guides/large-files.md b/docs/hi/guides/large-files.md new file mode 100644 index 0000000..2af1d65 --- /dev/null +++ b/docs/hi/guides/large-files.md @@ -0,0 +1,27 @@ +# बड़ी फ़ाइलें (स्ट्रीमिंग) + +`load()` पूरे ऑब्जेक्ट ग्राफ़ का निर्माण करता है। सैकड़ों एमबी के शब्दकोशों के लिए, स्ट्रीमिंग एपीआई सीमित मेमोरी में एक समय में एक प्रविष्टि को संसाधित करता है — वही `Entry` प्रकार, इसलिए एक मोड के लिए लिखा गया कोड दूसरे मोड में भी काम करता है। + +```python +import sil_lift + +with sil_lift.open_reader("big.lift") as reader: + header = reader.header # पहले से पार्स किया गया (एंट्रीज़ से पहले) + for entry in reader: # लेज़ी Iterator[Entry] + ... +``` + +```python +with sil_lift.open_reader("big.lift") as reader, sil_lift.open_writer( + "out.lift", header=reader.header, producer="my-script" +) as writer: + for entry in reader: + if not entry.date_deleted: # उदाहरण के लिए टॉम्बस्टोन हटाएँ + writer.write(entry) +``` + +टिप्पणियाँ: + +- लेखक का आउटपुट बिल्कुल वैसा ही होता है जैसा पूर्ण-दस्तावेज़ कैनोनिकल सीरियलाइज़र उसी सामग्री के लिए उत्पन्न करेगा — दोनों मोड कभी अलग नहीं होते। +- स्ट्रीमिंग मोड किसी भी स्रोत बाइट का पुन: उपयोग नहीं करता: आउटपुट हमेशा मानक होता है। रूट-स्तर का LIFT अवशेष — प्रविष्टियों के बीच की टिप्पणियाँ और स्कीमा-बाहर के गुण `` पर — शामिल नहीं किया जाता; प्रविष्टियाँ और हेडर पूर्ण होते हैं, अवशेष सहित। +- यदि `open_writer` ब्लॉक के शरीर में raise होता है, तो फ़ाइल स्पष्ट रूप से अधूरी छोड़ दी जाती है (कोई समापन `` नहीं) — एक आधा लिखा गया शब्दकोश पूर्ण नहीं दिखना चाहिए। diff --git a/docs/hi/guides/lift-export-interop.md b/docs/hi/guides/lift-export-interop.md new file mode 100644 index 0000000..d21718b --- /dev/null +++ b/docs/hi/guides/lift-export-interop.md @@ -0,0 +1,112 @@ +# अनुरूप LIFT का उत्पादन + +यह मार्गदर्शिका उन सभी के लिए है जो LIFT _एक्सपोर्टर_ लिख रहे हैं — किसी भी भाषा में ऐसा कोड जो किसी अन्य एप्लिकेशन के डेटा मॉडल को LIFT 0.13 में परिवर्तित करता है। `sil-lift` उस कार्य के लिए दो भूमिकाएँ निभाता है: एक अनुपालन द्वार जो आउटपुट की जाँच स्कीमा और उन अर्थों के अनुसार करता है जिन्हें स्कीमा व्यक्त नहीं कर सकती, और आउटपुट द्वारा अनुसरण किए जाने वाले आकारों और पाठ नियमों के लिए एक संदर्भ। + +LIFT लिखना इसे पार्स करने से कहीं आसान है: एक एक्सपोर्टर केवल उन संरचनाओं का उपसमूह उत्पन्न करता है जिन्हें उसका अपना मॉडल उत्पन्न करता है, और कभी भी पूरी विशिष्टता की वैकल्पिकताओं का सामना नहीं करता। कठिन हिस्सा विवरणों में है — `.lift-ranges` साथी, लेखन-प्रणाली-विशिष्ट पाठ, स्थिर आईडी, और XML एस्केपिंग — और ठीक इन्हीं को नीचे दिए गए चेक पकड़ते हैं। + +## ज़िप किए गए पैकेज + +LIFT आमतौर पर एक ही `.zip` फ़ाइल के रूप में स्थानांतरित किया जाता है — FieldWorks और The Combine दोनों इसी तरह आयात और निर्यात करते हैं — इसलिए `sil-lift` सीधे ज़िप किए गए पैकेज पढ़ता और लिखता है, चाहे इकोसिस्टम किसी भी लेआउट का उपयोग करे: आर्काइव रूट में फ़ाइलें, या एक शीर्ष-स्तरीय फ़ोल्डर के अंतर्गत नेस्टेड। + +- **पढ़ें:** `sil_lift.load("package.zip")` एक अस्थायी निर्देशिका में निकालता है, एकल `.lift` को ढूंढता है, और उसे लोड करता है (साथी और मीडिया सामान्य रूप से हल हो जाते हैं)। + - `validate`, `stats`, `check-media`, और `export` CLI कमांड्स `.zip` पथ भी स्वीकार करते हैं, इसलिए नीचे दिया गया गेट बिना किसी बदलाव के पैकेज पर चलता है। + - `stats` और `export` स्ट्रीम का उपयोग करें, और पूरे पैकेज के बजाय केवल `.lift` निकालें — ताकि मीडिया-भारी एक पर वे सस्ते रहें, और एक्सट्रैक्शन सीमा केवल `.lift` पर ही लागू हो, न कि इसके अलावा किसी और चीज़ पर। + - निकासी 10 जीआईबी और 100,000 सदस्यों तक सीमित है; किसी भी सीमा को पार करने वाले पैकेज को `LiftParseError` के साथ अस्वीकार कर दिया जाता है, ठीक वैसे ही जैसे उन पैकेजों को जिनके सदस्य पथ निकासी निर्देशिका से बाहर जाते हैं। +- `Write:` `Lexicon.save_zip("out.zip", wrap_folder="MyDict")` `.lift`, इसके `.lift-ranges`, और सोर्स फ़ोल्डर (media, `WritingSystems/`, `consent/`, ...) की हर दूसरी फ़ाइल को पैकेज करता है। एक ज़िप में + - `wrap_folder` डिफ़ॉल्ट रूप से ज़िप के नाम पर एक शीर्ष-स्तरीय फ़ोल्डर बनाता है (FieldWorks/Combine आयात कन्वेंशन); फ़्लैट आर्काइव के लिए `False` पास करें। + +`.lift` और `.lift-ranges` पैकेज के भीतर अपनी बाइट-निष्ठा बनाए रखते हैं; ज़िप कंटेनर स्वयं बाइट-पुनरुत्पादन योग्य नहीं है। + +## आउटपुट को एक अनुपालन गेट के रूप में मान्य करें। + +उत्पादित `.lift` फ़ाइल पर `sil-lift validate` पॉइंट करें। यह RELAX NG चलाता है (`.lift` और इसके `.lift-ranges` साथी दोनों पर) साथ ही ऐसी अर्थपूर्ण जाँचें भी करता है जिन्हें व्याकरण व्यक्त नहीं कर सकता: लटकते `relation`/`variant` संदर्भ, डुप्लिकेट GUIDs, रेंज-एलिमेंट के पैरेंट की अखंडता, उनके रेंज में परिभाषित नहीं किए गए trait और व्याकरण-सूचना मान, और हेडर `range/@href` संदर्भ जो किसी साथी पर हल नहीं होते। + +CI के लिए, किसी भी चीज़ में विफल होने पर मशीन-पठनीय निष्कर्ष उत्पन्न करें: + +``` +sil-lift validate export.lift --strict --no-check-media --format json +``` + +- `--strict` चेतावनियों (केवल त्रुटियों नहीं) के कारण रन विफल हो जाता है। +- `--no-check-media` फ़ाइलसिस्टम मीडिया-उपस्थिति जाँच को छोड़ देता है, जिसकी `missing-media` निष्कर्षों से शोर होता है जब ऑडियो/फ़ोटो फ़ाइलें CI में `.lift` के साथ एक ही फ़ोल्डर में नहीं होती हैं। +- `--format json` मानव-पठनीय टेक्स्ट के बजाय एक एकल JSON ऑब्जेक्ट (`{"problems": [...], "summary": {...}}`) प्रिंट करता है; इसके एग्जिट कोड और स्कीमा एक समर्थित, SemVer-आच्छादित इंटरफ़ेस हैं (देखें [कमांड लाइन गाइड](cli.md))। +- `--require-ids` अतिरिक्त रूप से उन प्रविष्टियों पर त्रुटि दिखाता है जिनमें `guid` नहीं है या उन सेंसों में जिनमें `id` नहीं है — यह तब उपयोगी होता है जब बाद में पुनः-आयात को डुप्लिकेट करने के बजाय अपडेट करना हो। + +मौन डेटा हानि (वह विफलता मोड जो फ्लैट CSV निर्यात को हानिकारक बना देता है) से बचने के लिए अपने स्रोत मॉडल पर `stats --format json` के साथ काउंट्स की पुष्टि करें: + +``` +sil-lift stats export.lift --format json +``` + +यह `entries`, `senses`, `examples`, `media_refs`, `languages` और प्रत्येक नाम के `traits` की संख्या रिपोर्ट करता है। + +### पाइथन टूलचेन के बिना गेट चलाना + +एक TypeScript या C# प्रोजेक्ट का CI, बंडल किए गए GitHub Action के माध्यम से, Python इंस्टॉल किए बिना ही वही जाँच चला सकता है: + +```yaml +- उपयोग: sillsdev/python-sil-lift@v0.1.0 + के साथ: + पथ: export.lift + सख्त: "true" + नो-चेक-मीडिया: "true" + प्रारूप: json +``` + +या रिपो के `Dockerfile` से बनी कंटेनर इमेज: + +``` +docker build -t sil-lift . +docker run --rm -v "$PWD:/work" -w /work sil-lift validate export.lift --strict +``` + +## `.lift-ranges` साथी + +नियंत्रित शब्दावलियाँ — शब्द-प्रकार, अर्थगत क्षेत्र, और कोई भी अन्य गुण-आधारित मान-समूह — एक सहोदर `.lift-ranges` फ़ाइल में रहती हैं, जिसे `
` से संदर्भित किया जाता है: + +```xml +
+ + + + +
+``` + +सहायक प्रत्येक श्रेणी की पूरी परिभाषा वहन करता है। मूल्य `` हैं; `parent` एक पदानुक्रम बनाता है; `label` / `abbrev` / `description` बहुपाठ हैं: + +```xml + + + + + +
संज्ञा
+
+
+ + + + + +
+``` + +तब एक प्रविष्टि आईडी द्वारा एक मान को संदर्भित करती है: एक सेंस का शब्द-प्रकार `` है, और एक सेमांटिक डोमेन `` है। `sil-lift validate` चेतावनी (`undefined-range-value`) देता है जब कोई मान अपनी सीमा में परिभाषित नहीं होता है, और त्रुटि (`range-parent`) देता है जब कोई `parent` सहोदर आईडी नहीं होता — इसलिए अपनी डेटा द्वारा वास्तव में उपयोग की जाने वाली सीमाएँ ही उत्सर्जित करें। यह भी देखें [दायरे और मीडिया](folder-media.md). + +यदि आप पाइथन में एक्सपोर्ट बनाते हैं, तो `Lexicon.add_ranges_file()`, `RangesFile.add_range()`, और `Range.add_element()` साथी (companion) का निर्माण करते हैं और आपके लिए हेडर संदर्भ जोड़ते हैं; `open_writer(..., ranges=...)` स्ट्रीमिंग पथ पर भी यही करता है। + +## पाठ और बहु-पाठ + +LIFT में प्रत्येक मानव-भाषा स्ट्रिंग एक _मल्टीटेक्स्ट_ है: प्रत्येक लेखन प्रणाली के लिए एक `
`, जो प्रत्येक एक `` को लपेटता है: + +```xml + + कंगा +
मुर्गी
+
+``` + +एक मॉडल जो स्ट्रिंग्स को भाषा कोड के आधार पर मैप करता है (एक `MultiString`, एक `Record`, एक `dict[str, str]`) इस एक-से-एक मैपिंग में बदल जाता है: प्रत्येक कुंजी के लिए एक प्रविष्टि एक `
` बन जाती है। एक ही मल्टीटेक्स्ट में प्रति भाषा अधिकतम एक फॉर्म की अनुमति है — अन्यथा `sil-lift` `duplicate-form-lang` चेतावनी देता है। + +XML एस्केपिंग ही एकमात्र वास्तव में शुद्धता-संवेदनशील हिस्सा है। एलिमेंट टेक्स्ट में `&`, `<`, and `>` को एस्केप करना होगा (`&`, `<`, `>`); एट्रिब्यूट मानों में उद्धरण चिह्न को भी। `sil-lift` के लेखक ठीक इन्हीं नियमों का पालन करते हैं और `` के भीतर कभी भी रिक्त स्थान (whitespace) को नहीं बदलते — यह वहाँ कोई इंडेंटेशन नहीं जोड़ता, क्योंकि इससे लेक्सिकल डेटा खराब हो जाएगा। यदि आप इसके आउटपुट से मेल खाने का लक्ष्य रखते हैं, तो एक वास्तविक XML सीरियलाइज़र की एस्केपिंग का पुन: उपयोग करें (स्वयं लिखित प्रतिस्थापन नहीं जो `&` को भूल जाता है) और `` सामग्री को बाइट-दर-बाइट उसी रूप में छोड़ दें जैसा आपके स्रोत में है। diff --git a/docs/hi/guides/read-edit-write.md b/docs/hi/guides/read-edit-write.md new file mode 100644 index 0000000..5cffeca --- /dev/null +++ b/docs/hi/guides/read-edit-write.md @@ -0,0 +1,66 @@ +# पढ़ें, संपादित करें, लिखें + +## लोड हो रहा है + +```python +import sil_lift + +lex = sil_lift.load("dictionary.lift") +``` + +`load()` कोई भी सुव्यवस्थित LIFT **0.13** दस्तावेज़ स्वीकार करता है — जिसमें स्कीमा-अमान्य वास्तविक-विश्व फ़ाइलें भी शामिल हैं। मॉडल द्वारा परिभाषित न किए गए कोई भी तत्व (अज्ञात तत्व/गुणधर्म, टिप्पणियाँ) प्रत्येक नोड के अपारदर्शी `extra` फ़ील्ड में LIFT अवशेष के रूप में बिना किसी हानि के संचित किया जाता है। अन्य LIFT संस्करण संस्करण का नाम बताते हुए `LiftParseError` उत्पन्न करते हैं। + +## मॉडल + +प्रत्येक LIFT तत्व एक प्रकारित डेटाक्लास है: `Entry`, `Sense`, `Example`, `Pronunciation`, `Variant`, `Relation`, `Etymology`, `Reversal`, आदि। बहुभाषी पाठ एक `Multitext` है, जो भाषा कोड से `Text` तक एक मैपिंग की तरह व्यवहार करता है: + +```python +entry = lex.find(id="abat") + +str(entry.lexical_unit["seh"]) # "abat" +entry.lexical_unit["en"] = "grove" # साधारण स्ट्रिंग्स को कोअर्स्ड किया जाता है +"en" in entry.citation # False +``` + +`Text` संरचित है — `str` और `Span` खंडों की एक क्रमबद्ध सूची — क्योंकि `` में घिरी हुई `` मार्कअप हो सकती है। `str(text)` सादे पाठ में बदल देता है; खंड राउंड-ट्रिपिंग के लिए मार्कअप बनाए रखते हैं। + +LIFT में ग्लॉसेज़ _फॉर्म-आकार के_ होते हैं (प्रत्येक `` अपनी भाषा स्वयं साथ ले चलता है), इसलिए एक सेंस में `glosses: list[Form]` होता है और साथ ही एक हेल्पर भी: + +```python +sense = entry.senses[0] +sense.gloss("en") # Text | None +entry.gloss_langs() # {"en", "id"} +``` + +## बचत + +```python +lex.save() # उस स्थान पर वापस जहाँ से इसे लोड किया गया था +lex.save("elsewhere.lift") +``` + +आपने जिन प्रविष्टियों में कोई बदलाव नहीं किया, वे बाइट-समान रूप से वापस लिखी जाती हैं; एक ऐसा दस्तावेज़ जिसे आपने बिल्कुल भी संशोधित नहीं किया, वह पहले बाइट से लेकर आखिरी बाइट तक बाइट-समान होता है। सटीक अनुबंध के लिए [फिडेलिटी गारंटी](../fidelity.md) देखें। + +## शुरू से निर्माण + +```python +lex = sil_lift.Lexicon(producer="my-script 1.0") +entry = sil_lift.Entry(id="hello", guid="...") +entry.lexical_unit["en"] = "hello" +sense = sil_lift.Sense() +sense.glosses.append(sil_lift.Form("fr", sil_lift.Text(["bonjour"]))) +entry.senses.append(sense) +lex.entries.append(entry) +lex.save("new.lift") +``` + +## कैनोनिकल क्रमबद्धकरण + +```python +lex.sort() # प्रविष्टियाँ (guid, id) के अनुसार; id/tag द्वारा रेंज/फ़ील्ड परिभाषाएँ +lex.save() # बिना बदले प्रविष्टियाँ अपने सटीक बाइट्स बनाए रखती हैं, नई व्यवस्था में + +sil_lift.canonicalize("in.lift", "out.lift") # पूरी तरह से पुनः-सीरियलाइज़्ड, diff-तैयार +``` + +यह भी देखें: [कार्य-उदाहरण: ग्लॉसेज़ के थोक-संपादन](bulk-edit-glosses.md)। diff --git a/docs/hi/guides/validate.md b/docs/hi/guides/validate.md new file mode 100644 index 0000000..69b903b --- /dev/null +++ b/docs/hi/guides/validate.md @@ -0,0 +1,36 @@ +# सत्यापित करें + +प्रमाणीकरण हमेशा स्पष्ट होता है — लोडिंग और सेविंग कभी भी निहित रूप से प्रमाणीकरण नहीं करते। + +```python +import sil_lift + +# Exhaustive: समस्याओं की एक लेज़ी स्ट्रीम (स्कीमा + सेमंटिक लेयर्स). +for problem in sil_lift.iter_problems("dictionary.lift"): + print(problem) + # error [dangling-ref] dictionary.lift:88 (entry apu): ref 'nope' matches ... + +# Fail-fast: पहली त्रुटि-स्तर की समस्या पर LiftValidationError उठाता है। +sil_lift.validate_file("dictionary.lift") + +# इन-मेमोरी स्थिति (पहले सीरियलाइज़ होती है — बड़े शब्दकोशों पर एक दस्तावेजीकृत लागत): +lex = sil_lift.load("dictionary.lift") +problems = list(lex.iter_problems()) +``` + +प्रत्येक `Problem` में `level` (`"error"`/`"warning"`), एक स्थिर `code`, `message`, और एक पता होता है: `file`, `entry_id`, `guid`, `line`. + +## परतें + +1. LIFT 0.13 व्याकरण के विरुद्ध RELAX NG (lift-standard से विक्रेता — इस पैकेज में प्रतिबद्ध एक बाइट-समान प्रति)। +2. **रेंज स्कीमा** — इस प्रोजेक्ट का `lift-ranges-0.13.rng` — प्रत्येक ट्रैक किए गए `.lift-ranges` साथी पर। +3. **सेमांटिक जाँचें** जिन्हें व्याकरण व्यक्त नहीं कर सकता: `duplicate-guid`, `dangling-ref`, `range-parent`, `undefined-range-value`, `duplicate-form-lang`, `missing-media`। + +## वास्तविक-विश्व फील्डवर्क्स (FLEx) आउटपुट + +FieldWorks व्यवस्थित रूप से कुछ सामग्री लिखता है जिसे सख्त टूलिंग अस्वीकार कर देती है। यहाँ sil-lift की नीति है, ताकि वास्तविक शब्दकोश उपयोगी रूप से मान्य हों: + +- `file://C:/...` hrefs (अमान्य URI) को स्कीमा त्रुटियों के रूप में नहीं, बल्कि **चेतावनियाँ** (`uri-not-rfc`) के रूप में रिपोर्ट किया जाता है — C# वैलिडेटर ने उन्हें कभी अस्वीकार नहीं किया। +- कानूनी रूप से इंटरलीव किए गए चाइल्ड एलिमेंट्स (उदाहरण के लिए, एक तरह से `field, note, field, note`) को **फ्लैग नहीं** किया जाता है, जो libxml2 में एक फॉलस पॉजिटिव से बचने का काम करता है। +- रेंज मानों की तुलना यूनिकोड NFC सामान्यीकरण के तहत की जाती है — FLEx एक ही एक्सपोर्ट में `.lift` को NFC में और `.lift-ranges` को NFD में लिखता है। +- FLEx के `trait`/`field` एक्सटेंशन `range-element` के अंदर रिपोर्ट किए जाते हैं (रेंज स्कीमा के खिलाफ स्कीमा त्रुटियाँ): ये वास्तव में विनिर्देश से विचलन हैं। diff --git a/docs/hi/index.md b/docs/hi/index.md new file mode 100644 index 0000000..3abd5c7 --- /dev/null +++ b/docs/hi/index.md @@ -0,0 +1,32 @@ +# सिल-लिफ्ट + +[LIFT](https://github.com/sillsdev/lift-standard) के लिए एक पाइथन लाइब्रेरी (लेक्सिकन इंटरचेंज फॉर्मैट) 0.13: LIFT फ़ोल्डर (`.lift` + `.lift-ranges` + मीडिया संदर्भ) का बिना हानि के रीड/राइट, स्कीमा और सेमांटिक सत्यापन, तथा मानक क्रमबद्धकरण — बड़े शब्दकोशों के लिए स्ट्रीमिंग एपीआई के साथ। + +स्थिति: प्री-रिलीज़, सक्रिय विकास के अधीन। + +## स्थापित करें + +[PyPI](https://pypi.org/project/sil-lift/) से: + +``` +pip install sil-lift # लाइब्रेरी + sil-lift कमांड +``` + +पाइथन 3.11+ आवश्यक है। एकमात्र रनटाइम निर्भरता lxml है। + +## 30-सेकंड का दौरा + +```python +import sil_lift + +lex = sil_lift.load("thesaurus.lift") # tracks .lift-ranges companions too + +for entry in lex.entries: + if "en" not in entry.gloss_langs(): + print(entry.id, str(entry.lexical_unit.get("seh") or "")) + +entry = lex.find(guid="0f5a9c3e-...") # या lex.find(id="hoofd_a1b2") +entry.senses[0].definition["en"] = "head (शरीर रचना)" + +lex.save() # बिना छुए प्रविष्टियाँ बाइट-समान; संपादित प्रविष्टि पुनः सीरियलाइज़्ड +``` diff --git a/docs/hi/reference.md b/docs/hi/reference.md new file mode 100644 index 0000000..7add820 --- /dev/null +++ b/docs/hi/reference.md @@ -0,0 +1,9 @@ +# एपीआई संदर्भ + +सार्वजनिक एपीआई बिल्कुल वही है जिसे `sil_lift` शीर्ष स्तर पर पुनः निर्यात करता है; अंडरस्कोर-पूर्वप्रत्यय वाले मॉड्यूल कार्यान्वयन विवरण हैं। + +::: sil_lift +options: +show_root_heading: false +show_source: false +members_order: source diff --git a/docs/ja/csharp-differences.md b/docs/ja/csharp-differences.md new file mode 100644 index 0000000..ef300ec --- /dev/null +++ b/docs/ja/csharp-differences.md @@ -0,0 +1,52 @@ +# C# ライブラリとの違い + +sil-lift は、SIL の C# LIFT ツール群――主に [libpalaso](https://github.com/sillsdev/libpalaso) 内の `SIL.Lift`(パーサー、バリデータ、マイグレーター、`LiftSorter`)、同じリポジトリ内の `SIL.DictionaryServices`(The Combine や WeSay で使用されている、独自の LIFT リーダー/ライターを備えた `LexEntry`/`LexSense` モデル)、および [Chorus](https://github.com/sillsdev/chorus) 内の LIFT ハンドラーなどです。 これは移植版ではなく、新規に実装されたものです。 このページでは、意図的に異なる仕様となっている点をまとめています。 + +## 適用範囲 + +| 機能 | C# ライブラリ | シルリフト | +| ---------- | -------------------------------------------------------- | ---------------------------------------------------- | +| LIFTのバージョン | 0.10–0.13(移行機能が組み込まれている) | **0.13のみ**;それより古いバージョンは明確なエラーで拒否されます | +| バージョンの移行 | `Migrator`(XSLTチェーン) | なし — 単発のアップグレードには、lift-standardに含まれるXSLTを使用してください | +| 3ウェイマージ/同期 | コーラス | 対象外 | +| 検証 | RELAX NG のみ(`Validator`) | RELAX NG + スキーマおよびセマンティックチェックの範囲 | +| ストリーミング | エントリ単位の内部解析 | パブリック `open_reader` / `open_writer` API | + +## APIの形状 + +`SIL.Lift`のパーサーはコールバック駆動型(`ILexiconMerger`)であり、解析イベントをコンシューマーにプッシュします。 その代わりに、sil-lift は単純なオブジェクトグラフ(LIFT の各要素に対応する型付きデータクラス)を返します。これは、Python スクリプト作成者がコールバックではなくオブジェクトを求めているためです。 `SIL.DictionaryServices` は、`SIL.Lift`の上に`LexEntry`/`LexSense`オブジェクトモデルを重ねていますが、アプリケーションモデルとしては、それらのアプリが使用する構造のみを表しています。そのため、これを通じて再シリアル化を行っても、sil-liftのLIFT残余処理やバイト単位の忠実度処理(後述)のように、モデル外のコンテンツを保持することはできません。 ストリーミングAPIは_同じ_ `Entry` 型を返すため、別途学習すべき簡略化されたモデルは存在しません。 + +## 往復の忠実度 + +最も顕著な意図的な違い。 `SIL.Lift` を使用して保存すると、ドキュメント全体が再シリアル化されます。 sil-liftの保証: + +- 変更のないドキュメントは**バイト単位で同一**に保存され、 +- 変更されていないエントリは、他のエントリが変更された場合でも、ソースのバイトデータをそのまま保持します。これは、Chorus が採用しているエントリ単位のバイトチャンキングと同じ仕組みであり、自動的に適用されます。 + +[フィデリティの保証](fidelity.md)をご覧ください。 + +## 検証 + +C#の`Validator`は、RELAX NGによる検証を1回実行し、最初に検出されたエラーを文字列として報告します。 sil-lift は、構造化された `Problem` ストリームを報告します。各ストリームには、対象となるファイル、エントリ、行が含まれており、そのスキーマ層は意図的に 3 箇所で異なる仕様となっています: + +- **無効なURIはエラーではなく警告です。** C#のRELAX NGエンジンは`anyURI`データ型を強制したことがなかったため、FieldWorks (FLEx) では長年にわたり、実際のレキシコンに`file://C:/...`というhrefを記述してきました。 それらのファイルを拒否すると、事実上すべてのFLExエクスポートにフラグが立てられてしまいます。 +- **Schematronルールが適用されます**(セマンティックチェックとして):LIFT文法における重複するフォーム言語や類似の共制約は、C#および生のlxmlによる検証のいずれにおいても、黙って無視されていました。 +- **ファイル間の比較はUnicode正規化されています**。これは、FLExが`.lift`ファイルをNFC形式で、関連する`.lift-ranges`ファイルをNFD形式で書き出すためです。 + +sil-lift は、読み込まれたレキシコンの `.lift-ranges` コンパニオンを、スタンドアロンの範囲ドキュメント用のスキーマ(ベースとなる LIFT 文法とともに `lift-standard` から提供されているもの)に対して検証します — `.lift` の検証が行われるたびに、追跡対象となっているすべての外部レンジファイルがチェックされます — C#の世界には、このようなスキーマ(またはチェック)は存在しません。 (`.lift` ファイルから切り離された状態で、`.lift-ranges` ファイル単体を検証するためのエントリポイントは存在しません。) + +## 規範的順序付け + +`Lexicon.sort()` は、`LiftSorter` の基本ルール(エントリは大文字小文字を区別しない GUID 順、範囲および範囲要素は ID 順、ヘッダーフィールドの定義はタグ順、意味の並びはファイル順のまま、`` 内の空白は一切変更しない)を踏襲していますが、3つの違いがあります: + +- GUID を持たないエントリは、ID に基づいて決定論的にソートされます(`LiftSorter` は GUID が存在することを前提としています)。 +- 順序付けはロケールに依存しません(大文字小文字を区別しないコードポイントに基づくものであり、.NET の不変文化ソート順序ではありません)。 +- ノート、関係、フォームなどの同種のリストは、キーに基づいて再ソートされるのではなく、ドキュメント内の順序を維持します。グループ化はすでに決定論的であり、順序を変更しても差分のノイズが増えるだけです。 + +仕様リポジトリの `canonicalizeLift.xsl` はまったく使用されていません。このXSLは、字句テキスト内の空白を削除してしまう(破壊的)うえ、生成されるIDが実行のたびに異なるからです。 + +## 繰り越されなかった + +- WeSay特有の利便性(ダッシュボード/LIFTファイルに関する設定処理)。 +- `SynchronicMerger`(Chorusの更新マージ)— バイト単位のチャンク化という考え方はフィデリティ層で引き継がれているが、マージ機能自体は引き継がれていない。 +- LDMLの表記体系の解析:`WritingSystems/` ディレクトリ内のファイルは、不透明なフォルダ内容として扱われます。 diff --git a/docs/ja/fidelity.md b/docs/ja/fidelity.md new file mode 100644 index 0000000..e3dcde2 --- /dev/null +++ b/docs/ja/fidelity.md @@ -0,0 +1,33 @@ +# フィデリティの保証 + +LIFTは_相互運用_フォーマットであるため、最初のルールは**理解できないものは決して削除しない**ことです。 `sil-lift`の契約。これは、実行のたびにテストスイートによって検証されます(コーパスファイルおよびプロパティベースの生成)。 + +## 読書 + +正しく構成された LIFT 0.13 ドキュメントであれば、スキーマに違反するコンテンツが含まれていても読み込まれます。 モデルで定義されていないものはすべて、最も近いノードの不透明な `Extras` コンテナに _LIFT 残余_ として格納されます。これは FieldWorks が同じ概念に対して用いる名称であり、`LiftResidue` フィールドに保存されます。これには、未知の属性や要素、XML コメントや処理命令、不要なテキスト、および形式不備の型付き属性などが含まれます (不正な日付は、`Extras`内に元の文字列のまま残され、型付きフィールドは`None`となります)。 + +## 変更のない文書を保存する + +編集を行わずに `load()` → `save()` を実行すると、**バイト単位で同一の出力**が生成されます。つまり、再フォーマット、再エスケープ、順序の変更は行われず、バイト順マークやXML宣言も含まれます。 現在、正規化リストは存在しません。恒等式は厳密です。 + +例外(ライターは完全なカノニカルシリアライゼーションにフォールバックします。これは意味的には完全ですが、バイト単位の整合性は保たれません): + +- ソースのエンコーディングがASCII互換ではない(UTF-8/US-ASCIIではない)、または +- ソースにDOCTYPEが含まれているか、または +- バイトスキャナとパーサーの間で、ドキュメントの最上位構造に関する認識に相違がある。例えば、仕様外の2つ目の `
` がある場合、パーサーはこれを1回だけ保持するが(スキャナは意図的に保守的であり、疑わしい場合はソースバイトを一切取得しない)、あるいは +- ソースはファイルから読み込まれるのではなく、メモリ上で構築されました。 + +## 編集したドキュメントの保存 + +- **変更されていないエントリは、元のバイト列そのままが出力されます。** エントリのモデルオブジェクトのいずれかの部分が parse 以降に変更された場合、そのエントリは変更済みとみなされます(これは、ダーティフラグではなく、正規シリアライゼーションのスナップショットによって検出されます)。 +- **変更されたエントリは、標準的な形式で完全に再シリアライズされます**:UTF-8、混合コンテンツの_外部_での2スペースのインデント(``および``内部の空白は一切変更されません)、要素ごとの文書化された子要素のグループ化(例:エントリ: 語彙単位、引用、発音、異形、意味、注釈、関係、語源、注釈、特徴、分野)、固定された属性順序、ISO-8601形式の日付(UTCの場合は`Z`)。 すべての残余データは再出力され、その位置は元の子インデックスに戻され、新しい子リストにクリップされます(これは近似処理であり、変更されていないエントリについてのみ、正確なバイト位置が保証されます)。 +- エントリの追加、削除、または順序変更を行うと、ドキュメント構造は再シリアル化されますが、変更されていないエントリのバイトデータはすべてそのまま出力されます。 + +!!! note ""ここでいう「Canonical" 」は、他のどのCanonical XMLとも関係ありません。" + このページにおける「標準形」とは、上記の箇条書きで説明されている、`sil-lift`独自の文書化された形状を指します。 これは、W3CのCanonical XML(C14N)プロセスとは無関係です。 これは、`SIL.Core` の `CanonicalXmlSettings` クラスとは無関係です。 + +## 既知の近似値(接触したノードのみ) + +- `` の実行の `内部` にあるコメントは保持されますが、正確な文字オフセットの位置には残されず、その実行の隣に移動されます。 +- 編集対象の要素内におけるクロス型の子要素の順序は、正規のグループ化に正規化されます(LIFTスキーマの `interleave` により、この順序は意味的に無意味となります)。 +- 存在はするが、フォームも残留物も一切含まないマルチテキスト要素(例:``)は、再出力されません。 このモデルでは、これらのフィールドを常に存在する `Multitext` として表現しています(`lexical-unit`、`citation`、`definition`、関係の `usage`、およびヘッダーの `label` / `abbrev` / `description`)として常に存在する `Multitext` として表現しているため、解析後は空の `Multitext` と存在しない `Multitext` を区別することができません。 意味的な情報は一切失われません。 diff --git a/docs/ja/guides/build-export.md b/docs/ja/guides/build-export.md new file mode 100644 index 0000000..280bb86 --- /dev/null +++ b/docs/ja/guides/build-export.md @@ -0,0 +1,168 @@ +# 実践例:LIFTエクスポートをゼロから作成する + +他のアプリケーションのデータをLIFTとしてエクスポートする場合――これは[準拠したLIFTの生成](lift-export-interop.md)で説明されている作業です――`sil-lift`を使用すれば、手動でXMLを生成する代わりに、ドキュメントをオブジェクト単位で構築してシリアライズすることができます。 ここでは、実際の辞書に含まれる要素(複数の表記体系、発音、例文付きの語義、イラスト、意味領域の特徴、およびアプリケーション固有のフィールド)を用いて項目を構築し、管理語彙を `.lift-ranges` コンパニオンに書き出し、検証を行い、保存するスクリプトの手順を解説します。 + +## 脚本 + +```python +from pathlib import Path + +import sil_lift + +lex = sil_lift.Lexicon(producer="my-exporter") + +# ソースモデルから構築された1つのエントリ。 +entry = sil_lift.Entry(id="kanga", guid="6b9e7c2a-3f4d-4a1b-8c5e-2d9f0a1b2c3d") +entry.lexical_unit["seh"] = "nkhuku" +entry.lexical_unit["pt"] = "galinha" + +pron = sil_lift.Pronunciation() +pron.forms["en"] = "Speaker: Ana" # The Combine の話者ラベルの規則 +pron.media.append(sil_lift.URLRef(href="audio/nkhuku.wav")) +entry.pronunciations.append(pron) + +sense = sil_lift.Sense(id="kanga_s1") +sense.grammatical_info = sil_lift.GrammaticalInfo(value="Noun") +sense.glosses.append(sil_lift.Form(lang="en", text=sil_lift.Text(["chicken"]))) +sense.definition["en"] = "a domestic fowl kept for its eggs and meat" + +example = sil_lift.Example() +example.forms["seh"] = "Ndinafuna nkhuku." +translation = sil_lift.Translation() +translation.forms["en"] = "I want a chicken." +example.translations.append(translation) +sense.examples.append(example) + +photo = sil_lift.URLRef(href="pictures/hen.jpg") +photo.label["en"] = "A hen" +sense.illustrations.append(photo) + +sense.traits.append(sil_lift.Trait(name="semantic-domain-ddp4", value="1.6.1.2")) + +scientific = sil_lift.Field(type="scientific-name") # アプリ固有の追加フィールド +scientific.content["en"] = "Gallus gallus domesticus" +sense.fields.append(scientific) + +entry.senses.append(sense) +lex.entries.append(entry) + +# エントリが参照する統制語彙(付随する .lift-ranges 内に記述)。 +ranges = sil_lift.RangesFile() +ranges.add_range("grammatical-info").add_element("Noun").label["en"] = "noun" +ranges.add_range("semantic-domain-ddp4").add_element("1.6.1.2").label["en"] = "Bird" +lex.add_ranges_file(ranges, href="birds.lift-ranges") + +# ディスクに書き込む前に、save() が書き込む内容を検証します。 +problems = list(lex.iter_problems()) +print(f"validation: {len(problems)} 個の問題") + +out = Path("export") +out.mkdir(exist_ok=True) +lex.save(out / "birds.lift") +print("=== birds.lift ===") +print((out / "birds.lift").read_text(encoding="utf-8"), end="") +print("=== birds.lift-ranges ===") +print((out / "birds.lift-ranges").read_text(encoding="utf-8"), end="") +``` + +## どのような成果が得られるか + +`validation: 0 problem(s)` と表示されたら、`.lift` とそのコンパニオンを並べて表示します: + +``` +=== birds.lift === + + +
+ + + + +
+ + + + nkhuku + +
+ galinha +
+
+ +
+ Speaker: Ana +
+ +
+ + + + chicken + + +
+ a domestic fowl kept for its eggs and meat +
+
+ +
+ Ndinafuna nkhuku. +
+ +
+ I want a chicken. +
+
+
+ + + + + +
+ Gallus gallus domesticus +
+
+
+
+
+=== birds.lift-ranges === + + + + + + + + + + + + + +``` + +## APIに関する注意事項 + +- マルチテキストフィールド(`lexical_unit`、`definition`、`Form`/`URLRef`のラベル、`Field`の内容など) マッピングインターフェースを通じて、各文字体系ごとに1つの文字列を取り込みます。`entry.lexical_unit["seh"] = "nkhuku"` とすると、`
` が追加されます。 言語コードをキーとする文字列を含むソースモデルは、これにそのまま対応します。 +- `RangesFile.add_range()` / `Range.add_element()` によって制御語彙が構築され、`Lexicon.add_ranges_file(ranges, href=...)` によってコンパニオンが紐付けられ、ヘッダー `` の参照が追加されます。これにより、エントリの `` および `` は、定義した範囲に対して解決されるようになります。 +- `URLRef` とは、href にオプションのキャプションやラベルなどのマルチテキストを加えたもので、``(音声)と ``(写真)の両方で使用されます。 ここでの発音は、The Combine の慣例に従い、`en` 形式で「 」と読みます。 +- ネイティブのLIFTホームライドが含まれないアプリ固有のデータは、``(または``)として指定されます。FieldWorksはこれらをカスタムフィールドとして読み取り、The Combineはそれらを保持します。 +- すべてのエントリに、実際の安定した `guid`(例:`uuid.uuid4()` から生成し、エクスポート間で再利用)を割り当ててください。そうすれば、後で再インポートした際に、エントリが複製されることなく、その場で更新されます。 `sil-lift validate --require-ids` はこのルールを強制します。 +- `lex.iter_problems()` は、データがディスクに書き込まれる前に、メモリ内のドキュメント(`save()` が書き込む内容)の整合性を検証します。ここでは、そのドキュメントに問題はありません。 この辞書にはまだフォルダが存在しないため、media-presence および companion-href のチェックはスキップされます。音声ファイルと写真ファイルが所定の場所に配置されたら、保存された出力に対して [`sil-lift validate`](cli.md) を実行してください(または `--no-check-media` オプションを指定して実行してください)。 + +## パッケージ + +`lex.save("export/birds.lift")` を実行すると、フォルダ形式(`.lift` と `.lift-ranges` を並べて配置)で保存されます。 FieldWorks や The Combine が直接インポートできる単一の ZIP パッケージを出力するには、代わりに `lex.save_zip("birds.zip")` を使用してください。詳細は [準拠した LIFT の生成](lift-export-interop.md) を参照してください。 diff --git a/docs/ja/guides/bulk-edit-glosses.md b/docs/ja/guides/bulk-edit-glosses.md new file mode 100644 index 0000000..5f3b1c7 --- /dev/null +++ b/docs/ja/guides/bulk-edit-glosses.md @@ -0,0 +1,79 @@ +# 実践例:注釈の一括編集 + +よくあるメンテナンス作業:辞書内のすべての英語用語について、ファイル内の他の部分を一切変更することなく、綴りを統一すること(英国式→米国式、またはその逆)。 ここでは、読み込み、編集、検証、保存を行う1つのスクリプトの手順を解説します。これにより、編集APIとフィデリティ保証が連携して機能する様子を確認できます。 + +## 脚本 + +```python +import sys + +import sil_lift + +path = "dictionary.lift" +lex = sil_lift.load(path) + + +def iter_senses(senses): + """すべての意味(サブ意味を含む)を再帰的にイテレートする。""" + for sense in senses: + yield sense + yield from iter_senses(sense.subsenses) + + +edited_glosses = 0 + +for entry in lex.entries: + for sense in iter_senses(entry.senses): + for gloss in sense.glosses: + if gloss.lang != "en": + continue + old = str(gloss.text) + new = old.replace("colour", "color") + if new != old: + gloss.text = sil_lift.Text([new]) + edited_glosses += 1 + +changed = lex.changed_entries() + +errors = [p for p in lex.iter_problems() if p.level == "error"] +if errors: + for problem in errors: + print(problem) + sys.exit(f"aborting: {len(errors)} validation error(s), nothing saved") + +lex.save() +print(f"edited {edited_glosses} gloss(es) across {len(changed)} entry(ies)") +``` + +いくつか注目すべき点があります: + +- `Sense.subsenses` 自体は `list[Sense]` であるため、`iter_senses` はこのリストを再帰的に処理します。もし `entry.senses` のみを走査する一括編集を行った場合、サブセンスの下にネストされた語義は、何の警告もなくスキップされてしまいます。 +- `gloss.text` は単なる文字列ではなく `Text` です。`str(gloss.text)` は照合のためにこれを平坦化し、置換結果は文字列そのものを変更するのではなく、`sil_lift.Text([new])` を使って書き戻されます。 +- `lex.changed_entries()` は、読み込まれたファイルと異なるエントリを報告します。 エントリのダイジェストはそのサブツリー全体を網羅しているため、ネストされたサブセンスを編集すると、そのサブセンスを含むエントリが報告されます。 + - シリアライズされたコンテンツを比較するため、フィールドに以前と同じ値を割り当てても、そのことは報告されません。 + - これはコンテンツの変更のみを報告します。`lex.added_entries()` および `lex.removed_entries()` は、読み込み以降に追加または削除されたエントリを対象としています。 + - `id` が重複していたり、存在しなかったりしても(LIFT ではこれが許可されています)、エントリそのものを返します。 + - 「数」として意味を持つのは、比較対象となるものが存在する場合に限られる。 バイトスキャナがソースの読み取りを拒否した場合(ASCII互換ではないエンコーディングや、スキャナとパーサーの不一致など)、基準となる状態が存在しないため、`changed_entries()` は _すべての_ エントリを報告します。 これはライトガードに関する正直な答えです。なぜなら、その場合は `save()` がファイル全体を再シリアル化するためですが、その結果、カウント値は編集部分のサイズではなく、レキシコンのサイズになってしまうからです。 +- `lex.changes()` は、ドキュメントに _何らかの変更_ があったかどうかを報告します。 これは、エントリだけでなく、ヘッダー、ルート要素、およびすべての `.lift-ranges` コンパニオンも対象としています。 + - これは、`save()` がソースバイトを再現する場合にのみ偽となるため、不要な書き込みをスキップするには `if not lex.changes(): ...` という書き方が正しい。 この保証は一方向のみ有効です。つまり、書き換えられるはずのドキュメントについては「書き込む内容なし」と報告されることは決してありませんが、完全な再シリアル化を余儀なくされる変更であっても、元のバイト列に戻った場合でも、依然として報告されることがあります。 + - これは保存先ではなく内容を比較するため、この方法ではインプレース保存のみを保護するようにしてください。`lex.save(some_other_dir / "dictionary.lift")` は、変更の有無にかかわらず、まだ何も格納されていない場所にドキュメントとその関連ファイルを書き込みます。 + - これはガードであり、処理を高速化するものではありません。この処理に応答するとすべてのエントリが処理されますが、これは`save()`がどのソースバイトを再利用できるかを判断するために行う作業と同じです。したがって、省略されるのは書き込みそのもの(ファイルの最終更新時刻が変わらず、不要な差分が発生しない)であり、判断にかかる労力ではありません。 +- メモリ内での検証(`lex.iter_problems()`)では、まず編集後の状態をシリアル化するため、ディスクへの書き込みが行われる前に、編集内容が正しく反映されます。 `"error"` レベルの `Problem` が発生した場合は処理を中止します(警告については呼び出し側が判断することになります)。これにより、不正な編集内容が `save()` に到達することはありません。 + +この方法で触れてみる価値があるのは、グロスだけではありません。 この `Multitext` マッピング領域は、定義や、エントリや意味に含まれるその他のすべての多言語フィールドにも同様に適用されます: + +```python +sense.definition["en"] = "物体の色" +``` + +## 実行する + +「colour」と記載された語義説明と、その下位意味の語義説明の両方が含まれる小規模な語彙リストに対して照合を行う: + +``` +1件のエントリに含まれる2つの用語を編集しました +``` + +## 忠実度の見返り + +この保証は「エントリ」単位で適用されます。モデルが変更されていないエントリは、読み込まれたときと**バイト単位で同一**な状態で返され、実際に変更を加えたエントリのみが再シリアル化されます。 上記の実行では、1つのエントリの注釈が編集されましたが、ファイル内のその他のエントリはすべて、バイト単位でそのままの状態で保持されました。 (粒度に注意してください:エントリの任意の部分を編集すると、変更されていない同義語を含め、そのエントリ全体が再シリアライズされます。) したがって、5万項目の辞書にある1つの用語を編集しても、再フォーマットされたファイルが生成されるのではなく、1つの項目に影響する差分が生成されるだけである。 契約の詳細については、[フィデリティの保証](../fidelity.md)をご覧ください。 diff --git a/docs/ja/guides/cli.md b/docs/ja/guides/cli.md new file mode 100644 index 0000000..39a31e4 --- /dev/null +++ b/docs/ja/guides/cli.md @@ -0,0 +1,70 @@ +# コマンドライン + +パッケージをインストールすると(`pip install sil-lift`)、`sil-lift` コマンドもインストールされます。これは、LiftTools の精神に基づいた、パッケージに同梱されているサポートツールです(また、`validate` については、ライブラリ API の実用例も含まれています)。 + +``` +sil-lift validate PATH [--format {text,json}] [--strict] [--no-check-media] [--require-ids] + すべての問題(ファイル/エントリ/行を指定);エラー時は終了コード 1 +sil-lift stats PATH [--format {text,json}] + エントリ/センス/言語のカウント(ストリーミング;サイズ不問) +sil-lift sort PATH [-o OUT] 正規順にソートされ、差分比較可能なコピー(デフォルト:その場で処理) +sil-lift check-media PATH 欠落および孤立したメディアのレポート;欠落がある場合は 1 で終了 +sil-lift export PATH [-o OUT] [--langs L] [--tsv] + リーフセンスごとに1行(サブセンスは平坦化)でCSV/TSV形式に出力(ストリーミング) +``` + +`--format json` を指定すると、CIや自動化処理で使用できるよう、単一のJSONオブジェクトが標準出力に書き出されます(それ以外は何も出力されません)。スキーマについては、以下の例を参照してください。 `--strict` オプションは、警告をエラーとして扱い、警告が見つかった場合は終了値 1 を返します。エラーだけでなく、警告が一切ない場合にのみビルドを成功させるようにしたい場合にこのオプションを使用してください。 `--no-check-media` オプションを指定すると、ファイルシステムのメディア存在確認がスキップされ(`missing-media` の検出結果が表示されなくなります)。これは、生成されたばかりのエクスポートファイルに含まれる音声ファイルや写真ファイルが、同じフォルダ内ではなく別の場所に保存されている場合に、そのエクスポートファイルを検証する際に役立ちます。 `--require-ids` は、`guid` がないエントリや `id` がないセンスに対しても(`missing-id` エラーとして)失敗します。これは、安定した ID を使用して再インポートを行うワークフローにおいて、LIFT よりも厳格な仕様となっています。 パスとして `-` を指定すると、ドキュメントは標準入力(stdin)から読み込まれます(パイプで渡されたドキュメントにはフォルダがないため、それに付随する `.lift-ranges` やメディアは解決されません)。 `stats` も同様に `--format json` を受け付け、集計結果を単一の JSON オブジェクトとして出力します。 + +!!! note + `validate` の終了コードおよび `--format json` のスキーマは、サポートされている自動化インターフェースです。これらはいずれもテストの対象となっており、SemVer に基づいてのみ変更されます。 + +`sort` は `.lift` ファイルのみを上書きします。関連する `.lift-ranges` ファイルは変更されません +(これらを並べ替えるには、`RangesFile` API を別途使用してください)。 + +`validate`、`stats`、`check-media`、および `export` も、ZIP形式のLIFTパッケージ(アーカイブのルートにファイルが配置されている形式、またはトップレベルのフォルダの下にネストされている形式のいずれかの `.zip` ファイル)を受け付けます。このパッケージは一時ディレクトリに展開され、コマンドの実行完了後に削除されます。 ストリーミングコマンド `stats` および `export` は `.lift` ファイルのみを抽出するため、メディアデータが大量に含まれるパッケージでも処理負荷が低くなります。一方、`validate` および `check-media` はフォルダ全体を必要とし、その内容をすべて抽出します。 + +例: + +``` +$ sil-lift validate dictionary.lift +エラー [dangling-ref] dictionary.lift:88 (エントリ apu): 参照 'nope' に一致するエントリ ID/GUID または意味 ID がありません +警告 [uri-not-rfc] dictionary.lift:6:: URI 権限として Windows ドライブ文字が使用されています (FLEx 形式の file://C:/) +エラー 1 件、警告 1 件 + +$ sil-lift validate dictionary.lift --format json +{ + "problems": [ + { + "level": "error", + "code": "dangling-ref", + "message": "ref 'nope' matches no entry id/guid or sense id", + "file": "dictionary.lift", + "entry_id": "apu", + "guid": null, + "line": 88 + }, + { + "level": "warning", + "code": "uri-not-rfc", + "message": ": URIの権限としてWindowsのドライブ文字が使用されています (FLEx形式の file://C:/)", + "file": "dictionary.lift", + "entry_id": null, + "guid": null, + "line": 6 + } + ], + "summary": { + "errors": 1, + "warnings": 1 + } +} + +$ sil-lift stats sango.lift +entries: 3507 +senses: 4541 +... + +$ sil-lift export dictionary.lift --langs en,fr -o dictionary.csv +``` + +終了コード:`0`:成功(`--strict` オプションが指定されていない限り、警告は許容される)、`1`:問題が見つかった(検証エラー/メディアの欠落/`--strict` オプション指定時の警告)、`2`:入力が読み取れない。 diff --git a/docs/ja/guides/folder-media.md b/docs/ja/guides/folder-media.md new file mode 100644 index 0000000..b03fa72 --- /dev/null +++ b/docs/ja/guides/folder-media.md @@ -0,0 +1,41 @@ +# 「LIFT」フォルダ:範囲とメディア + +LIFTレキシコンは通常、_フォルダ_として構成されます。その中には、`.lift`ファイル、1つ以上の`.lift-ranges`ファイル(サイドカーファイル)、および`audio/` / `pictures/`フォルダ内のメディアファイルが含まれます。 + +## 範囲 + +```python +lex = sil_lift.load("dictionary.lift") # コンパニオンは自動的に追跡される + +lex.ranges_files # {Path(...): RangesFile} +lex.all_ranges() # マージされた {id: Range} ビュー +lex.all_ranges()["grammatical-info"].elements +``` + +コンパニオンディスカバリーは実世界の状況を適切に処理します。既存のファイルを指す `range/@href` が使用されます。 FieldWorksの未参照の絶対パス `file://C:/...` のhrefは、`.lift`の隣にあるhrefのベース名にフォールバックします。また、従来の `.lift-ranges` という同階層のファイルは、参照元がなくても自動的に検出されます。 + +`lex.save()` は、`.lift` と追跡対象のすべてのコンパニオンをまとめて書き込みます。 `RangesFile` への編集内容は、そのファイルに保存されます。変更されていない範囲については、バイト単位でそのまま保持されます。 単体での使用: + +```python +ranges = sil_lift.RangesFile.load("dictionary.lift-ranges") +ranges.find("grammatical-info") +ranges.sort() +ranges.save() +``` + +`load()` に `resolve_ranges=False` を渡すと、コンパニオンの検出をスキップできます。 + +## メディア + +```python +for ref in lex.media_refs(): # すべての および + print(ref.kind, ref.href, ref.entry_id) + +lex.missing_media() # ファイルが存在しない参照 +``` + +解決方法は従来のレイアウトに従います。相対的な href は指定されたままの形式でチェックされ(バックスラッシュは正規化されます — WeSay は `pictures\photo with space.png` と記述します)、`audio/`(発音用メディアの場合)または `pictures/`(イラストの場合)の下に配置されます。 リモートまたは絶対パスの href は検証できないため、スキップされます。 + +## その他のフォルダ内の内容 + +LIFTフォルダには、sil-liftがモデル化していないファイルがしばしば格納されています。たとえば、`WritingSystems/` にある文字体系の LDML ファイルや、`consent/` にある The Combine の話者同意に関する音声・画像ファイルなどです。 `load()`/`save()`はこれらをそのままにしておき、[`Lexicon.save_zip()`](lift-export-interop.md)はフォルダをパッケージ化する際、これらをそのまま引き継ぎます。 diff --git a/docs/ja/guides/large-files.md b/docs/ja/guides/large-files.md new file mode 100644 index 0000000..1b4065e --- /dev/null +++ b/docs/ja/guides/large-files.md @@ -0,0 +1,27 @@ +# 大容量ファイル(ストリーミング) + +`load()` はオブジェクトグラフ全体を構築します。 数百MB規模の辞書の場合、ストリーミングAPIは、制限されたメモリ内で1回に1つのエントリを処理します。エントリの型は同じ`Entry`であるため、一方のモード向けに記述されたコードは、もう一方のモードでも動作します。 + +```python +import sil_lift + +with sil_lift.open_reader("big.lift") as reader: + header = reader.header # 最初に解析される(エントリより先) + for entry in reader: # 遅延評価される Iterator[Entry] + ... +``` + +```python +with sil_lift.open_reader("big.lift") as reader, sil_lift.open_writer( + "out.lift", header=reader.header, producer="my-script" +) as writer: + for entry in reader: + if not entry.date_deleted: # 例:トゥームストーンを削除 + writer.write(entry) +``` + +注記: + +- このライタの出力は、同じコンテンツに対してフルドキュメントの正規シリアライザが生成するものと完全に一致します。この2つのモードの間で結果にずれが生じることはありません。 +- ストリーミングモードでは、ソースのバイトは再利用されません。出力は常に正規化された状態になります。 ルートレベルのLIFT残余(``上のエントリ間のコメントやスキーマ外属性)は転送されません。エントリとヘッダーは、残余を含めて完全な状態で転送されます。 +- `open_writer` ブロックの本体で例外が発生した場合、ファイルは明らかに未完了の状態(`` による閉じ処理が行われていない)のまま残されます。つまり、書きかけの辞書は完了しているように見えてはなりません。 diff --git a/docs/ja/guides/lift-export-interop.md b/docs/ja/guides/lift-export-interop.md new file mode 100644 index 0000000..323f537 --- /dev/null +++ b/docs/ja/guides/lift-export-interop.md @@ -0,0 +1,112 @@ +# 準拠したLIFTの生成 + +このガイドは、LIFT _エクスポーター_ を開発するすべての方を対象としています。これは、任意の言語で記述された、他のアプリケーションのデータモデルを LIFT 0.13 形式に変換するコードのことです。 `sil-lift` は、その作業において 2 つの役割を果たします。1 つは、出力がスキーマに準拠しているかを確認する適合性チェック機能と、スキーマでは表現できない意味論のチェック機能であり、もう 1 つは、出力が従わなければならない形状やテキストに関する規則の参照基準としての役割です。 + +LIFTの記述は、その解析よりもはるかに簡単です。エクスポート機能は、自身のモデルが生成する構文のサブセットのみを出力するため、仕様全体のオプション性に対処する必要が一切ないからです。 難しいのは細部、つまり `.lift-ranges` コンパニオン、文字体系ごとのテキスト、安定した ID、XML エスケープなどですが、これらはまさに以下のチェックで検出される項目です。 + +## ZIP形式のパッケージ + +LIFTは通常、単一の`.zip`ファイルとして移動されます(FieldWorksとThe Combineはどちらもこの形式でインポートおよびエクスポートを行います)。そのため、`sil-lift`は、エコシステムで使用されるいずれのレイアウト(アーカイブのルートにファイルが配置されている場合、またはトップレベルのフォルダの下にネストされている場合)においても、zip形式のパッケージを直接読み書きします。 + +- **解説:** `sil_lift.load("package.zip")` は、一時ディレクトリに解凍を行い、単一の `.lift` ファイルを検出して読み込みます(コンパニオンやメディアは通常通り解決されます)。 + - `validate`、`stats`、`check-media`、および`export`の各CLIコマンドも`.zip`形式のパスを指定できるため、以下のゲートはパッケージをそのままの状態で実行します。 + - `stats` および `export` ストリームから、パッケージ全体ではなく `.lift` のみを抽出します。これにより、メディアデータが大量に含まれるパッケージでも処理負荷を抑えることができ、抽出制限は `.lift` に対してのみ適用され、それ以外の部分には適用されません。 + - 抽出の上限は10 GiBおよび100,000個のメンバーです。いずれかの制限を超えるパッケージは、メンバーのパスが抽出ディレクトリの範囲外にある場合と同様に、`LiftParseError`が発生して拒否されます。 +- **記述例:** `Lexicon.save_zip("out.zip", wrap_folder="MyDict")` とすると、`.lift`、その `.lift-ranges`、およびソースフォルダ内のその他のすべてのファイル(media、`WritingSystems/`、`consent/` など)がまとめてパッケージ化されます。 zipファイルにまとめる。 + - `wrap_folder` のデフォルト値は、zip ファイル名と同じ名前の最上位フォルダです(FieldWorks/Combine のインポート規約に従います)。フラットなアーカイブにする場合は、`False` を指定してください。 + +`.lift` および `.lift-ranges` は、パッケージ内ではバイト単位の忠実性を維持しますが、zip コンテナ自体はバイト単位で再現可能ではありません。 + +## 出力を適合性ゲートとして検証する + +生成された `.lift` ファイルを `sil-lift validate` の対象として指定します。 RELAX NG(`.lift` およびそのコンパニオンである `.lift-ranges` の両方に対して)を実行するほか、文法では表現できないセマンティックチェックも行います: ぶら下がった `relation`/`variant` の参照、重複する GUID、範囲要素の親の整合性、範囲内で定義されていない特性および文法情報の値、およびコンパニオンに解決されないヘッダー `range/@href` の参照などです。 + +CIでは、何か問題が発生した場合は失敗とし、機械可読な結果を出力する: + +``` +sil-lift validate export.lift --strict --no-check-media --format json +``` + +- `--strict` を指定すると、エラーだけでなく警告も実行の失敗原因となります。 +- `--no-check-media` を指定すると、ファイルシステムのメディア存在確認がスキップされます。CIにおいて、オーディオファイルや写真ファイルが `.lift` ファイルと同じフォルダにない場合、この確認で検出される `missing-media` の結果はノイズとなるためです。 +- `--format json` を指定すると、人間が読みやすいテキストの代わりに単一の JSON オブジェクト (`{"problems": [...], "summary": {...}}`) が出力されます。その終了コードとスキーマは、SemVer に準拠したサポート対象のインターフェースとなっています([コマンドラインガイド](cli.md)を参照してください)。 +- `--require-ids` オプションを指定すると、`guid` が欠落しているエントリや `id` が欠落しているエントリに対して追加でエラーが発生します。これは、後で再インポートを行う際に、重複を避けるために更新のみを行う必要がある場合に役立ちます。 + +ソースモデルに対して `stats --format json` を使用してカウントを確認することで、サイレントデータ損失(フラットな CSV エクスポートでデータが失われる原因となる障害モード)を防ぐことができます: + +``` +sil-lift の統計データのエクスポート export.lift --format json +``` + +このレポートでは、`entries`、`senses`、`examples`、`media_refs`、`languages`、および名称ごとの`traits`の件数が報告されます。 + +### Pythonツールチェーンを使用せずにgateを実行する + +TypeScript または C# プロジェクトの CI では、Python をインストールしなくても、バンドルされている GitHub Action を通じて同じチェックを実行できます: + +```yaml +- uses: sillsdev/python-sil-lift@v0.1.0 + with: + path: export.lift + strict: "true" + no-check-media: "true" + format: json +``` + +あるいは、リポジトリの `Dockerfile` からビルドされたコンテナイメージ: + +``` +docker build -t sil-lift . +docker run --rm -v "$PWD:/work" -w /work sil-lift validate export.lift --strict +``` + +## `.lift-ranges` コンパニオン + +統制語彙(品詞、意味領域、およびその他の特性キー付き値セット)は、`
` から参照される同階層の `.lift-ranges` ファイルに格納されています: + +```xml +
+ + + + +
+``` + +このガイドブックには、各シリーズの完全な定義が掲載されています。 値は `` です。`parent` は階層を構築します。`label` / `abbrev` / `description` はマルチテキストです: + +```xml + + + + + +
n
+
+
+ + + + + +
+``` + +エントリは、ID によって値を参照します。例えば、品詞は ``、意味領域は `` となります。 `sil-lift validate` は、値がその範囲内で定義されていない場合に警告(`undefined-range-value`)を出し、`parent` が兄弟要素の ID ではない場合にエラー(`range-parent`)を返します。したがって、データで実際に使用されている範囲を指定してください。 「範囲とメディア」も参照してください(folder-media.md)。 + +Pythonでエクスポートを構築する場合、`Lexicon.add_ranges_file()`、`RangesFile.add_range()`、および`Range.add_element()`がコンパニオンを構築し、ヘッダー参照を自動的に追加してくれます。 `open_writer(..., ranges=...)` は、ストリーミングパス上でも同様の処理を行います。 + +## テキストおよびマルチテキスト + +LIFT におけるすべての自然言語文字列は _マルチテキスト_ です。つまり、各文字体系ごとに 1 つの `
` があり、それぞれが `` を囲んでいます: + +```xml + + カンガ +
+
+``` + +言語コードをキーとする文字列のモデル(`MultiString`、`Record`、`dict[str, str]`)は、これと一対一で対応します。つまり、キー1つにつき1つのエントリが、1つの `
` になります。 1つのマルチテキスト内で、1つの言語につき最大1つのフォームまでしか許可されません。それ以外の場合、`sil-lift`は`duplicate-form-lang`という警告を出します。 + +XMLのエスケープ処理こそが、唯一、正確さが極めて重要となる部分です。 要素のテキスト内では、`&`、`<`, and `>` はエスケープする必要があります(`&`、`<`、`>`)。属性値内では、引用符も同様にエスケープする必要があります。 `sil-lift`の作者はまさにこれらのルールを適用しており、``内の空白を一切変更しません。そこではインデントを追加しないのは、そうすることで字句データが破損してしまうためです。 その出力を再現したい場合は、本格的なXMLシリアライザのエスケープ処理を再利用し(`&`を忘れるような手作りの置換処理は避ける)、`` のコンテンツはソースにあるまま、バイト単位でそのまま残してください。 diff --git a/docs/ja/guides/read-edit-write.md b/docs/ja/guides/read-edit-write.md new file mode 100644 index 0000000..0cd5de6 --- /dev/null +++ b/docs/ja/guides/read-edit-write.md @@ -0,0 +1,66 @@ +# 読む、編集する、書く + +## 読み込み中 + +```python +import sil_lift + +lex = sil_lift.load("dictionary.lift") +``` + +`load()` は、スキーマに準拠していない実世界のファイルを含め、形式が正しい LIFT **0.13** ドキュメントであればどれでも受け付けます。 モデルで定義されていないもの(未知の要素や属性、コメントなど)は、各ノードの不透明な `extra` フィールドに LIFT 残余として、情報を失うことなく保持されます。 その他の LIFT バージョンでは、そのバージョン名を指定した `LiftParseError` が発生します。 + +## モデル + +LIFTの各要素は、型付きデータクラスです。具体的には、`Entry`、`Sense`、`Example`、`Pronunciation`、`Variant`、`Relation`、`Etymology`、`Reversal`などがあります。 多言語テキストは `Multitext` であり、言語コードから `Text` へのマッピングのように動作します: + +```python +entry = lex.find(id="abat") + +str(entry.lexical_unit["seh"]) # "abat" +entry.lexical_unit["en"] = "grove" # プレーン文字列は型変換される +"en" in entry.citation # False +``` + +`Text` は、`str` および `Span` のフラグメントからなる順序付きリストとして構成されています。これは、`` にネストされた `` マークアップが含まれる可能性があるためです。 `str(text)` はプレーンテキストに変換されますが、フラグメントは往復処理のためにマークアップを維持します。 + +LIFT では、グロスは _Form 型_ です(各 `` は独自の言語を持ちます)。したがって、センスには `glosses: list[Form]` と、ヘルパー関数が定義されています: + +```python +sense = entry.senses[0] +sense.gloss("en") # Text | None +entry.gloss_langs() # {"en", "id"} +``` + +## 保存 + +```python +lex.save() # 読み込まれた場所に戻す +lex.save("elsewhere.lift") +``` + +変更を加えなかったエントリは、**バイト単位で同一**な状態で書き戻されます。まったく変更を加えなかったドキュメントは、最初のバイトから最後のバイトまでバイト単位で同一です。 契約の詳細については、[フィデリティの保証](../fidelity.md)をご覧ください。 + +## ゼロから構築する + +```python +lex = sil_lift.Lexicon(producer="my-script 1.0") +entry = sil_lift.Entry(id="hello", guid="...") +entry.lexical_unit["en"] = "hello" +sense = sil_lift.Sense() +sense.glosses.append(sil_lift.Form("fr", sil_lift.Text(["bonjour"]))) +entry.senses.append(sense) +lex.entries.append(entry) +lex.save("new.lift") +``` + +## 規範的順序付け + +```python +lex.sort() # エントリを (guid, id) の順に並べ替え; ID/タグごとの範囲/フィールド定義 +lex.save() # 変更されていないエントリは、新しい順序のまま正確なバイトデータを保持します + +sil_lift.canonicalize("in.lift", "out.lift") # 完全に再シリアル化され、差分比較の準備が整いました +``` + +関連項目:[実践例:用語の一括編集](bulk-edit-glosses.md)。 diff --git a/docs/ja/guides/validate.md b/docs/ja/guides/validate.md new file mode 100644 index 0000000..56a6bba --- /dev/null +++ b/docs/ja/guides/validate.md @@ -0,0 +1,36 @@ +# 検証 + +検証は常に明示的に行われます。読み込みや保存の際、暗黙的に検証が行われることはありません。 + +```python +import sil_lift + +# 網羅的:Problem(スキーマ層+セマンティック層)の遅延ストリーム。 +for problem in sil_lift.iter_problems("dictionary.lift"): + print(problem) + # エラー [dangling-ref] dictionary.lift:88 (entry apu): ref 'nope' matches ... + +# フェイルファースト:最初のエラーレベルの問題で LiftValidationError を発生させます。 +sil_lift.validate_file("dictionary.lift") + +# メモリ内状態(最初にシリアライズされる — 大規模な辞書では、ドキュメントに記載されているようにコストがかかる): +lex = sil_lift.load("dictionary.lift") +problems = list(lex.iter_problems()) +``` + +各 `Problem` には、`level`(`"error"`/`"warning"`)、一意の `code`、`message`、および `file`、`entry_id`、`guid`、`line` というアドレス情報が含まれています。 + +## レイヤー + +1. **RELAX NG** による LIFT 0.13 文法との照合(lift-standard からベンダー提供されたもの — 本パッケージにコミットされたバイト単位で同一のコピー)。 +2. **Rangesスキーマ** — このプロジェクトの `lift-ranges-0.13.rng` — は、追跡対象のすべての `.lift-ranges` コンパニオンに対して適用されます。 +3. **文法では表現できないセマンティックチェック**:`duplicate-guid`、`dangling-ref`、`range-parent`、`undefined-range-value`、`duplicate-form-lang`、`missing-media`。 + +## 実環境におけるFieldWorks(FLEx)の出力結果 + +FieldWorksは、厳格なツールによって拒否されるようなコンテンツを体系的に生成してしまう。 実際の辞書が有用に検証されるよう、sil-liftの方針は以下の通りです: + +- `file://C:/...` という href(無効な URI)は、スキーマエラーではなく**警告**(`uri-not-rfc`)として報告されます。C# バリデータはこれらを拒否したことはありません。 +- 法的に交互に配置された子要素(ある意味では `field, note, field, note` など)にはフラグが付けられず、これにより libxml2 における誤検知を回避しています。 +- 範囲値は、UnicodeのNFC正規化に基づいて比較されます。FLExは、同じエクスポート内で`.lift`をNFCで記述しますが、`.lift-ranges`はNFDで記述します。 +- `range-element` 内の FLEx の `trait`/`field` 拡張は、**報告される**(range スキーマに対するスキーマエラーとして):これらは紛れもない仕様からの逸脱である。 diff --git a/docs/ja/index.md b/docs/ja/index.md new file mode 100644 index 0000000..708a57a --- /dev/null +++ b/docs/ja/index.md @@ -0,0 +1,32 @@ +# シルリフト + +[LIFT](https://github.com/sillsdev/lift-standard) 用の Python ライブラリ (Lexicon Interchange FormaT) 0.13: LIFTフォルダ(`.lift` + `.lift-ranges` + メディア参照)のロスレスな読み書き、スキーマおよびセマンティック検証、正規ソート機能 — 大規模なレキシコン向けのストリーミングAPIを搭載。 + +**ステータス:プレリリース版、現在開発中。** + +## インストール + +[PyPI](https://pypi.org/project/sil-lift/)より: + +``` +pip install sil-lift # ライブラリ + sil-lift コマンド +``` + +Python 3.11 以降が必要です。 実行時の依存関係は lxml のみです。 + +## 30秒のツアー + +```python +import sil_lift + +lex = sil_lift.load("thesaurus.lift") # .lift-ranges のコンパニオンも追跡する + +for entry in lex.entries: + if "en" not in entry.gloss_langs(): + print(entry.id, str(entry.lexical_unit.get("seh") or "")) + +entry = lex.find(guid="0f5a9c3e-...") # または lex.find(id="hoofd_a1b2") +entry.senses[0].definition["en"] = "head (anatomy)" + +lex.save() # 変更のないエントリはバイト単位で同一のまま。編集されたエントリは再シリアル化される +``` diff --git a/docs/ja/reference.md b/docs/ja/reference.md new file mode 100644 index 0000000..34eba16 --- /dev/null +++ b/docs/ja/reference.md @@ -0,0 +1,9 @@ +# APIリファレンス + +パブリックAPIとは、まさに`sil_lift`が最上位レベルで再エクスポートしているものであり、アンダースコアで始まるモジュールは実装上の詳細です。 + +::: sil_lift +options: +show_root_heading: false +show_source: false +members_order: source diff --git a/docs/pt/csharp-differences.md b/docs/pt/csharp-differences.md new file mode 100644 index 0000000..49ac0ba --- /dev/null +++ b/docs/pt/csharp-differences.md @@ -0,0 +1,52 @@ +# Diferenças em relação às bibliotecas do C\# + +O sil-lift é, de certa forma, análogo às ferramentas LIFT da SIL para C# — principalmente o `SIL.Lift` em [libpalaso](https://github.com/sillsdev/libpalaso) (analisador sintático, validador, migrador, `LiftSorter`), o `SIL.DictionaryServices` no mesmo repositório (o modelo `LexEntry`/`LexSense`, com o seu próprio leitor/gravador LIFT, utilizado pelo The Combine e pelo WeSay), e os manipuladores LIFT no [Chorus](https://github.com/sillsdev/chorus). Trata-se de uma nova implementação, não de uma adaptação. Esta página resume os casos em que o comportamento difere deliberadamente. + +## Âmbito + +| Capacidade | Bibliotecas C# | sil-lift | +| ------------------------------- | -------------------------------------------------------- | ----------------------------------------------------------------------------------------- | +| Versões do LIFT | 0,10–0,13 (migração integrada) | **Apenas 0.13**; as versões mais antigas são rejeitadas com um erro claro | +| Migração de versão | `Migrator` (cadeia XSLT) | nenhuma — utilizar os XSLT do «lift-standard» para atualizações pontuais | +| Fusão/sincronização a três vias | Refrão | fora do âmbito | +| Validação | Apenas RELAX NG (`Validator`) | RELAX NG + verificações de esquema e semânticas | +| Streaming | análise sintática com granularidade interna das entradas | API pública `open_reader` / `open_writer` | + +## Estrutura da API + +O analisador do `SIL.Lift` é orientado por callbacks (`ILexiconMerger`): envia eventos de análise a um consumidor. Em vez disso, o sil-lift devolve um gráfico de objetos simples — classes de dados tipadas para cada elemento LIFT — porque os programadores de Python querem objetos, e não callbacks. O `SIL.DictionaryServices` sobrepõe um modelo de objetos `LexEntry`/`LexSense` ao `SIL.Lift`, mas, enquanto modelo de aplicação, representa apenas as construções que essas aplicações utilizam — pelo que a resserialização através dele não consegue preservar o conteúdo fora do modelo da mesma forma que o tratamento de resíduos LIFT e a fidelidade de bytes do sil-lift o fazem (ver abaixo). A API de streaming devolve o _mesmo_ tipo `Entry`, pelo que não há um segundo modelo simplificado para aprender. + +## Fidelidade de ida e volta + +A diferença deliberada mais marcante. Ao guardar com o `SIL.Lift`, todo o documento é novamente serializado. A sil-lift garante: + +- um documento inalterado é guardado com **identidade de bytes**, e +- As entradas não alteradas mantêm os seus bytes de origem exatos, mesmo quando outras entradas são alteradas — trata-se do mesmo agrupamento de bytes por entrada que o Chorus utiliza, aplicado automaticamente. + +Consulte [Garantias da Fidelity](fidelity.md). + +## Validação + +O `Validator` do C# executa uma passagem RELAX NG e apresenta os primeiros erros sob a forma de cadeias de caracteres. O sil-lift apresenta um fluxo estruturado de `Problemas`, cada um contendo o ficheiro, a entrada e a linha a que se refere, e a sua camada de esquema diverge deliberadamente em três pontos: + +- **Os URIs inválidos são avisos, não erros.** O motor RELAX NG do C# nunca impôs o tipo de dados `anyURI`, pelo que o FieldWorks (FLEx) tem vindo a inserir hrefs do tipo `file://C:/...` em léxicos reais há anos. A rejeição desses ficheiros faria com que praticamente todas as exportações do FLEx fossem sinalizadas. +- **As regras do Schematron são aplicadas** (como verificações semânticas): as linguagens de formulário duplicadas e as co-restrições semelhantes na gramática LIFT foram ignoradas silenciosamente tanto pela validação em C# como pela validação direta do lxml. +- **As comparações entre ficheiros são normalizadas segundo o Unicode**, uma vez que o FLEx grava o ficheiro `.lift` em NFC e o ficheiro associado `.lift-ranges` em NFD. + +O sil-lift também valida os ficheiros `.lift-ranges` associados a um léxico carregado, comparando-os com um esquema para documentos de intervalos autónomos (fornecido pelo `lift-standard` juntamente com a gramática LIFT de base) — todos os ficheiros de intervalos externos monitorizados são verificados sempre que o `.lift` é validado — sem que exista tal esquema (ou verificação) no mundo do C#. (Não existe um ponto de entrada para validar um ficheiro `.lift-ranges` por si só, separado de um ficheiro `.lift`.) + +## Ordenação canónica + +O `Lexicon.sort()` reflete as regras fundamentais do `LiftSorter` (entradas por GUID, sem distinção entre maiúsculas e minúsculas; intervalos e elementos de intervalo por ID; definições de campos de cabeçalho por etiqueta; significados mantidos na ordem do ficheiro; espaços em branco dentro de `` nunca são alterados), com três diferenças: + +- as entradas sem um GUID são ordenadas de forma determinística por ID (o `LiftSorter` pressupõe a existência de um GUID); +- a ordenação é independente da configuração regional (pontos de código com maiúsculas e minúsculas ignoradas, e não a ordenação de cultura invariante do .NET); +- As listas do mesmo tipo, como notas, relações e formulários, mantêm a ordem do documento em vez de serem reordenadas por chave — o agrupamento já é determinístico e reordená-las apenas acrescenta ruído às diferenças. + +O ficheiro `canonicalizeLift.xsl` do repositório de especificações não é utilizado de todo: elimina os espaços em branco no texto lexical (de forma destrutiva) e os identificadores gerados variam em cada execução. + +## Não transitado + +- Funcionalidades específicas do WeSay (painel de controlo/gestão de configurações relacionadas com ficheiros LIFT). +- `SynchronicMerger` (fusão de atualizações do Chorus) — o conceito de divisão em blocos de bytes mantém-se na camada de fidelidade, mas a fusão já não. +- Análise do sistema de escrita LDML: os ficheiros na pasta `WritingSystems/` são tratados como conteúdo opaco da pasta. diff --git a/docs/pt/fidelity.md b/docs/pt/fidelity.md new file mode 100644 index 0000000..51c71b3 --- /dev/null +++ b/docs/pt/fidelity.md @@ -0,0 +1,33 @@ +# Garantias de fidelidade + +O LIFT é um formato de _intercâmbio_, pelo que a primeira regra é **nunca ignorar aquilo que não compreendes**. O contrato do `sil-lift`, verificado pelo conjunto de testes em cada execução (ficheiros de corpus e geração baseada em propriedades): + +## Leitura + +Qualquer documento LIFT 0.13 bem formado é carregado — mesmo que inclua conteúdo inválido em termos de esquema. Tudo o que o modelo não definir é transportado no contentor opaco `Extras` do nó mais próximo como _resíduo LIFT_ — nome dado pelo FieldWorks a este conceito, que é armazenado num campo `LiftResidue`: atributos e elementos desconhecidos, comentários XML e instruções de processamento, texto disperso e atributos tipados malformados (uma data inválida permanece como a cadeia de caracteres original em `Extras`; o campo tipado é `None`). + +## Guardar um documento sem alterações + +`load()` → `save()`, sem alterações, gera uma **saída byte a byte idêntica** — sem reformatação, sem reescapamento, sem reordenação, incluindo marcas de ordem de bytes e declarações XML. De momento, não existe nenhuma lista de normalização: a identidade é exata. + +Exceções (o gravador recorre à serialização canónica completa, que é semanticamente completa, mas não preserva os bytes): + +- a codificação da fonte não é compatível com ASCII (não é UTF-8/US-ASCII), ou +- o código-fonte contém um DOCTYPE, ou +- o scanner de bytes e o analisador não estão de acordo quanto à estrutura de nível superior do documento — por exemplo, um segundo `
` fora das especificações, que o analisador mantém apenas uma vez (o scanner é deliberadamente conservador: em caso de dúvida, não captura quaisquer bytes da fonte), ou +- o código-fonte foi compilado na memória, em vez de ser carregado a partir de um ficheiro. + +## Guardar um documento editado + +- **As entradas não alteradas são emitidas tal como estão, a partir dos seus bytes originais.** Uma entrada é considerada alterada se alguma parte do seu objeto-modelo tiver sido alterada desde a análise (detetada através de um instantâneo de serialização canónica, e não por um indicador de alteração). +- **As entradas alteradas são re-serializadas de forma canónica e completa**: UTF-8, indentação com 2 espaços _fora_ do conteúdo misto (os espaços dentro de `` e `` nunca são alterados), um agrupamento de elementos filhos documentado por elemento (por exemplo, entrada: unidade lexical, citação, pronúncias, variantes, sentidos, notas, relações, etimologias, anotações, características, campos), ordem fixa dos atributos, datas em ISO-8601 (`Z` para UTC). Todos os resíduos são reemitidos; a sua posição é restaurada no índice filho original, limitada à nova lista de filhos (uma aproximação — as posições exatas em bytes só são garantidas para entradas que não foram alteradas). +- Adicionar, remover ou reordenar entradas faz com que a estrutura do documento seja novamente serializada, mas continua a emitir, tal como estão, os bytes de todas as entradas que não sofreram alterações. + +!!! note ""O XML canónico" aqui apresentado não está relacionado com nenhum outro XML canónico" + A forma canónica nesta página refere-se à forma documentada do próprio `sil-lift`, descrita num ponto anterior. Não tem qualquer relação com o processo «Canonical XML» (C14N) do W3C. Não tem qualquer relação com a classe `CanonicalXmlSettings` do `SIL.Core`. + +## Aproximações conhecidas (apenas nós tocados) + +- Os comentários _dentro_ de uma execução `` são preservados, mas são movidos para junto da execução, não sendo mantidos na sua posição exata em termos de caracteres. +- A ordem cruzada de elementos filhos dentro de um elemento editado é normalizada para o agrupamento canónico (o `interleave` do esquema LIFT torna esta ordem semanticamente insignificante). +- Um elemento multitext que esteja presente mas que não contenha nada — nem formas, nem resíduos, por exemplo, `` — não é reemitido. O modelo representa estes campos como um `Multitext` sempre presente (`unidade-lexical`, `citação`, `definição`, o `uso` de uma relação e `rótulo` / `abbrev` / `description` em referências de URL, intervalos, elementos de intervalo e no cabeçalho), pelo que um campo vazio é indistinguível de um ausente após a análise. Não se perde nada em termos semânticos. diff --git a/docs/pt/guides/build-export.md b/docs/pt/guides/build-export.md new file mode 100644 index 0000000..0dd291c --- /dev/null +++ b/docs/pt/guides/build-export.md @@ -0,0 +1,168 @@ +# Exemplo prático: criar uma exportação LIFT a partir do zero + +Se estiver a exportar dados de outra aplicação no formato LIFT — a tarefa subjacente a [Produção de LIFT em conformidade](lift-export-interop.md) — o `sil-lift` pode construir o documento objeto a objeto e serializá-lo, em vez de gerar XML manualmente. Este guia explica passo a passo um script que cria uma entrada com os elementos que um dicionário real possui (vários sistemas de escrita, uma pronúncia, um significado com um exemplo, uma ilustração, uma característica do domínio semântico e um campo específico da aplicação), grava os vocabulários controlados num ficheiro complementar `.lift-ranges`, valida e guarda. + +## O guião + +```python +from pathlib import Path + +import sil_lift + +lex = sil_lift.Lexicon(producer="my-exporter") + +# Uma entrada, criada a partir do modelo de origem. +entry = sil_lift.Entry(id="kanga", guid="6b9e7c2a-3f4d-4a1b-8c5e-2d9f0a1b2c3d") +entry.lexical_unit["seh"] = "nkhuku" +entry.lexical_unit["pt"] = "galinha" + +pron = sil_lift.Pronunciation() +pron.forms["en"] = "Speaker: Ana" # A convenção de rotulagem de falantes do Combine +pron.media.append(sil_lift.URLRef(href="audio/nkhuku.wav")) +entry.pronunciations.append(pron) + +sense = sil_lift.Sense(id="kanga_s1") +sense.grammatical_info = sil_lift.GrammaticalInfo(value="Substantivo") +sense.glosses.append(sil_lift.Form(lang="en", text=sil_lift.Text(["chicken"]))) +sense.definition["en"] = "uma ave doméstica criada pelos seus ovos e carne" + +example = sil_lift.Example() +example.forms["seh"] = "Ndinafuna nkhuku." +translation = sil_lift.Translation() +translation.forms["en"] = "I want a chicken." +example.translations.append(translation) +sense.examples.append(example) + +photo = sil_lift.URLRef(href="pictures/hen.jpg") +photo.label["en"] = "Uma galinha" +sense.illustrations.append(photo) + +sense.traits.append(sil_lift.Trait(name="semantic-domain-ddp4", value="1.6.1.2")) + +scientific = sil_lift.Field(type="scientific-name") # um campo extra específico da aplicação +scientific.content["en"] = "Gallus gallus domesticus" +sense.fields.append(scientific) + +entry.senses.append(sense) +lex.entries.append(entry) + +# Os vocabulários controlados a que a entrada se refere, num ficheiro .lift-ranges. +ranges = sil_lift.RangesFile() +ranges.add_range("grammatical-info").add_element("Noun").label["en"] = "noun" +ranges.add_range("semantic-domain-ddp4").add_element("1.6.1.2").label["en"] = "Bird" +lex.add_ranges_file(ranges, href="birds.lift-ranges") + +# Validar o que o save() escreveria, antes de gravar no disco. +problems = list(lex.iter_problems()) +print(f"validação: {len(problems)} problema(s)") + +out = Path("export") +out.mkdir(exist_ok=True) +lex.save(out / "birds.lift") +print("=== birds.lift ===") +print((out / "birds.lift").read_text(encoding="utf-8"), end="") +print("=== birds.lift-ranges ===") +print((out / "birds.lift-ranges").read_text(encoding="utf-8"), end="") +``` + +## O que produz + +`validação: 0 problema(s)`, e, em seguida, o `.lift` e o seu equivalente lado a lado: + +``` +=== birds.lift === + + +
+ + + + +
+ + + + nkhuku + +
+ galinha +
+
+ +
+ Speaker: Ana +
+ +
+ + + + chicken + + +
+ a domestic fowl kept for its eggs and meat +
+
+ +
+ Ndinafuna nkhuku. +
+ +
+ I want a chicken. +
+
+
+ + + + + +
+ Gallus gallus domesticus +
+
+
+
+
+=== birds.lift-ranges === + + + + + + + + + + + + + +``` + +## Notas sobre a API + +- Campos multitexto (`lexical_unit`, `definition`, um rótulo de `Form`/`URLRef`, o conteúdo de um `Field`, ...) selecione uma sequência por sistema de escrita através da interface de mapeamento: `entry.lexical_unit["seh"] = "nkhuku"` adiciona um `
`. Um modelo de origem que indexa cadeias de caracteres pelo código do idioma corresponde diretamente a isto. +- `RangesFile.add_range()` / `Range.add_element()` criam os vocabulários controlados, e `Lexicon.add_ranges_file(ranges, href=...)` associa o vocabulário complementar e adiciona as referências de cabeçalho `` — para que os campos `` e `` da entrada sejam resolvidos de acordo com os intervalos que definiu. +- Um `URLRef` é um href acompanhado de um texto múltiplo opcional (legenda/rótulo) — utilizado tanto para `` (áudio) como para `` (fotografias). A pronúncia aqui segue a convenção do The Combine, que prevê uma forma «en» que se lê «Orador: ». +- Dados específicos da aplicação que não incluam viagens de regresso a casa nativas do LIFT, como « » (ou « »): o FieldWorks interpreta-os como campos personalizados e o The Combine preserva-os. +- Atribua a cada entrada um `guid` válido e estável (por exemplo, gerado por `uuid.uuid4()`, reutilizado em todas as exportações) — uma reimportação posterior atualiza a entrada no local, em vez de a duplicar. O comando `sil-lift validate --require-ids` garante que isso seja cumprido. +- A função `lex.iter_problems()` valida o documento na memória (o que a função `save()` iria gravar) antes de qualquer coisa ser gravada no disco; neste caso, está correto. Como o léxico ainda não tem nenhuma pasta, as verificações de «media-presence» e «companion-href» são ignoradas — execute [`sil-lift validate`](cli.md) na saída guardada (ou com `--no-check-media`) assim que os ficheiros de áudio e de fotografias estiverem no local. + +## Embalagem + +`lex.save("export/birds.lift")` grava a estrutura da pasta (ficheiros `.lift` e `.lift-ranges` lado a lado). Para gerar um único pacote compactado que o FieldWorks e o The Combine importem diretamente, utilize `lex.save_zip("birds.zip")` — consulte [Produção de LIFT em conformidade](lift-export-interop.md). diff --git a/docs/pt/guides/bulk-edit-glosses.md b/docs/pt/guides/bulk-edit-glosses.md new file mode 100644 index 0000000..3402a28 --- /dev/null +++ b/docs/pt/guides/bulk-edit-glosses.md @@ -0,0 +1,79 @@ +# Exemplo prático: edição em massa de glossários + +Uma tarefa de manutenção comum: uniformizar a ortografia em todas as entradas em inglês de um léxico (britânica → americana, ou vice-versa) sem alterar qualquer outro aspeto do ficheiro. Este exemplo apresenta um script que carrega, edita, valida e guarda — demonstrando como a API de edição e a garantia de fidelidade funcionam em conjunto. + +## O guião + +```python +import sys + +import sil_lift + +path = "dictionary.lift" +lex = sil_lift.load(path) + + +def iter_senses(senses): + """Retorna todos os sentidos, incluindo os sub-sentidos (recursivo).""" + for sense in senses: + yield sense + yield from iter_senses(sense.subsenses) + + +edited_glosses = 0 + +for entry in lex.entries: + for sense in iter_senses(entry.senses): + for gloss in sense.glosses: + if gloss.lang != "en": + continue + old = str(gloss.text) + new = old.replace("colour", "color") + if new != old: + gloss.text = sil_lift.Text([new]) + edited_glosses += 1 + +changed = lex.changed_entries() + +errors = [p for p in lex.iter_problems() if p.level == "error"] +if errors: + for problem in errors: + print(problem) + sys.exit(f"interrupção: {len(errors)} erro(s) de validação, nada guardado") + +lex.save() +print(f"gloss(es) editado(s) {edited_glosses} em {len(changed)} entrada(s)") +``` + +Algumas coisas que vale a pena referir: + +- `Sense.subsenses` é, por si só, uma `list[Sense]`, pelo que `iter_senses` a percorre de forma recursiva — uma edição em massa que apenas percorresse `entry.senses` ignoraria silenciosamente qualquer gloss aninhado sob um subsense. +- `gloss.text` é um `Text`, não uma string simples: `str(gloss.text)` simplifica-o para efeitos de correspondência, e a substituição é gravada novamente com `sil_lift.Text([new])`, em vez de alterar a string no local. +- A função `lex.changed_entries()` indica quais as entradas que diferem do ficheiro tal como foi carregado. Uma vez que o resumo de uma entrada abrange toda a sua subárvore, uma alteração num sub-significado aninhado é comunicada à entrada que o contém. + - Como compara conteúdo serializado, a atribuição a um campo do valor que este já possuía não é registada. + - Apresenta apenas alterações de conteúdo; as funções `lex.added_entries()` e `lex.removed_entries()` abrangem as entradas que surgiram ou desapareceram desde o carregamento. + - Devolve as próprias entradas, sem que a existência de `id` duplicados ou ausentes (o que o LIFT permite) tenha qualquer influência. + - Enquanto contagem, só faz sentido quando há algo com que comparar. Quando o analisador de bytes não consegue ler a fonte — seja por se tratar de uma codificação não compatível com ASCII, seja devido a uma incompatibilidade entre o analisador e o parser —, não existe uma linha de base e a função `changed_entries()` reporta _todas_ as entradas. Essa é a resposta correta no que diz respeito à proteção contra gravação, uma vez que, nesse caso, o `save()` volta a serializar o ficheiro na íntegra, mas isso significa que a contagem corresponde ao tamanho do léxico e não ao tamanho da edição. +- A função `lex.changes()` indica se o documento sofreu _alguma_ alteração. Abrange não só as entradas, mas também o cabeçalho, o elemento raiz e todos os elementos com a classe `.lift-ranges`. + - Só é falso quando a função `save()` reproduz os bytes de origem, o que faz com que `if not lex.changes(): ...` seja a forma correta de evitar uma gravação desnecessária. A garantia funciona num único sentido: nunca indica «nada para registar» relativamente a um documento que seria reescrito, enquanto que uma alteração que obrigue a uma resseriação completa pode resultar nos mesmos bytes originais e, mesmo assim, ser registada. + - Como compara o conteúdo e não o destino, utilize-o apenas para guardar no próprio local: `lex.save(some_other_dir / "dictionary.lift")` grava o documento e os seus ficheiros associados num local que ainda não contém nada, independentemente de ter havido ou não alterações. + - Trata-se de uma medida de proteção, não de um acelerador — ao responder a esta chamada, todas as entradas são processadas, o que corresponde ao trabalho que a função `save()` realiza para determinar quais os bytes de origem que pode reutilizar; assim, o que se salta é a própria gravação (sem alteração na data de modificação do ficheiro, sem diferenças espúrias), e não o esforço de tomar essa decisão. +- A validação na memória (`lex.iter_problems()`) serializa primeiro o estado editado, para que este reflita corretamente a edição antes de qualquer coisa ser gravada no disco. A interrupção ao ocorrer qualquer `Problem` de nível `"error"` — os avisos são deixados para que quem chamou a função decida o que fazer — significa que uma edição incorreta nunca chega à função `save()`. + +Os glosses não são a única coisa que vale a pena tratar desta forma. A mesma superfície de mapeamento `Multitext` aplica-se às definições e a todos os outros campos multilingues de uma entrada ou significado: + +```python +sense.definition["en"] = "a cor de uma coisa" +``` + +## Executá-lo + +Execute a pesquisa com um pequeno léxico que contenha uma entrada e uma subentrada cujos significados sejam ambos «cor»: + +``` +editou 2 glossários em 1 entrada(s) +``` + +## A recompensa pela fidelidade + +A garantia aplica-se a cada _entrada_: uma entrada cujo modelo não tenha sido alterado é devolvida **identicamente em termos de bytes** à forma como foi lida, e apenas as entradas em que se interveio efetivamente são resserializadas. Na execução acima, foram editadas as anotações de uma entrada — todas as outras entradas do ficheiro mantiveram os seus bytes exatos. (Repare na granularidade: a edição de qualquer parte de uma entrada faz com que toda a entrada seja novamente serializada, incluindo os significados associados que não foram alterados.) A edição de uma definição num léxico com 50 000 entradas resulta, portanto, num ficheiro «diff» que afeta apenas uma entrada, e não num ficheiro reformatado. Consulte [Garantias da Fidelity](../fidelity.md) para conhecer o contrato na íntegra. diff --git a/docs/pt/guides/cli.md b/docs/pt/guides/cli.md new file mode 100644 index 0000000..8e6f951 --- /dev/null +++ b/docs/pt/guides/cli.md @@ -0,0 +1,70 @@ +# A linha de comandos + +A instalação do pacote (`pip install sil-lift`) instala também o comando `sil-lift` — uma ferramenta compatível, no espírito do LiftTools, que vem incluída no pacote (e, no caso do `validate`, um exemplo prático da API da biblioteca). + +``` +sil-lift validate PATH [--format {text,json}] [--strict] [--no-check-media] [--require-ids] + todos os problemas, com indicação de ficheiro/entrada/linha; saída 1 em caso de erros +sil-lift stats PATH [--format {text,json}] + contagens de entradas/sentidos/línguas (em fluxo; qualquer tamanho) +sil-lift sort PATH [-o OUT] cópia ordenada canonicamente, pronta para comparação (por predefinição: no local) +sil-lift check-media PATH relatório de meios em falta e órfãos; sai com código 1 se houver meios em falta +sil-lift export PATH [-o OUT] [--langs L] [--tsv] + uma linha por sentido folha (subsentidos achatados) para CSV/TSV (em fluxo) +``` + +`--format json` escreve um único objeto JSON na saída padrão (e nada mais) para utilização em CI/automatização; consulte o esquema no exemplo abaixo. A opção `--strict` trata os avisos como erros, devolvendo o valor 1 caso seja detetado algum — utilize-a para condicionar a compilação à ausência total de avisos, em vez de apenas à ausência de erros. `--no-check-media` ignora a verificação da presença de ficheiros multimédia no sistema de ficheiros (suprimindo os resultados de `missing-media`), o que é útil ao validar uma exportação recém-gerada cujos ficheiros de áudio/fotografias se encontram noutro local e não na mesma pasta. `--require-ids` também falha (com um erro `missing-id`) em qualquer entrada que não tenha um `guid` ou em qualquer sentido que não tenha um `id` — sendo mais rigoroso do que o LIFT, para fluxos de trabalho que reimportam através de um id estável. Ao passar `-` como caminho, o documento é lido a partir do stdin (um documento transmitido por canalização não tem pasta, pelo que o ficheiro `.lift-ranges` associado e os ficheiros multimédia não são resolvidos). O `stats` também aceita a opção `--format json`, apresentando as contagens como um único objeto JSON. + +!!! note + Os códigos de saída do `validate` e o esquema `--format json` constituem uma interface de automatização suportada: ambos são abrangidos por testes e só sofrem alterações de acordo com a SemVer. + +O `sort` reescreve apenas o ficheiro `.lift`; os ficheiros `.lift-ranges` associados permanecem inalterados +(organize-os separadamente com a API `RangesFile`). + +Os comandos `validate`, `stats`, `check-media` e `export` também aceitam um pacote LIFT compactado (um ficheiro `.zip` com qualquer um dos dois formatos — ficheiros na raiz do arquivo ou aninhados numa pasta de nível superior); este é extraído para um diretório temporário e eliminado quando o comando termina. Os comandos de streaming `stats` e `export` extraem apenas o próprio ficheiro `.lift`, pelo que o seu custo de execução é reduzido em pacotes com muitos ficheiros multimédia; os comandos `validate` e `check-media` necessitam da pasta completa e extraem todo o seu conteúdo. + +Exemplos: + +``` +$ sil-lift validate dictionary.lift +erro [dangling-ref] dictionary.lift:88 (entrada apu): a referência «nope» não corresponde a nenhum ID/GUID de entrada nem a nenhum ID de sentido +aviso [uri-not-rfc] dictionary.lift:6: : Letra de unidade do Windows utilizada como autoridade URI (estilo FLEx file://C:/) +1 erro(s), 1 aviso(s) + +$ sil-lift validate dictionary.lift --format json +{ + "problems": [ + { + "level": "error", + "code": "dangling-ref", + "message": "a referência 'nope' não corresponde a nenhum ID de entrada/GUID ou ID de sentido", + "file": "dictionary.lift", + "entry_id": "apu", + "guid": null, + "line": 88 + }, + { + "level": "warning", + "code": "uri-not-rfc", + "message": ": Letra de unidade do Windows utilizada como autoridade URI (file://C:/ ao estilo FLEx)", + "file": "dictionary.lift", + "entry_id": null, + "guid": null, + "line": 6 + } + ], + "summary": { + "errors": 1, + "warnings": 1 + } +} + +$ sil-lift stats sango.lift +entradas: 3507 +sentidos: 4541 +... + +$ sil-lift export dictionary.lift --langs en,fr -o dictionary.csv +``` + +Códigos de saída: `0` sucesso (são permitidos avisos, a menos que se utilize a opção `--strict`), `1` resultados (erros de validação / ficheiros multimédia em falta / avisos quando se utiliza a opção `--strict`), `2` entrada ilegível. diff --git a/docs/pt/guides/folder-media.md b/docs/pt/guides/folder-media.md new file mode 100644 index 0000000..4199934 --- /dev/null +++ b/docs/pt/guides/folder-media.md @@ -0,0 +1,41 @@ +# A pasta LIFT: gamas e suportes + +Um léxico LIFT é normalmente uma _pasta_: o ficheiro `.lift`, um ou mais ficheiros complementares `.lift-ranges` (ficheiros «sidecar») e os ficheiros multimédia nas pastas `audio/` e `pictures/`. + +## Intervalos + +```python +lex = sil_lift.load("dictionary.lift") # companheiros rastreados automaticamente + +lex.ranges_files # {Path(...): RangesFile} +lex.all_ranges() # vista {id: Range} combinada +lex.all_ranges()["grammatical-info"].elements +``` + +A descoberta de ficheiros associados lida com o mundo real: é utilizado um `range/@href` que aponta para um ficheiro existente; Os hrefs absolutos «file://C:/...» do FieldWorks que não têm referência recuam para o nome base do href junto ao «.lift»; e o irmão convencional «.lift-ranges» é selecionado mesmo quando nada o referencia. + +A função `lex.save()` grava o ficheiro `.lift` e todas as funções complementares monitorizadas em conjunto. As alterações feitas num `RangesFile` são guardadas no _seu_ ficheiro; os intervalos que não foram alterados mantêm os seus bytes exatos. Utilização autónoma: + +```python +ranges = sil_lift.RangesFile.load("dictionary.lift-ranges") +ranges.find("grammatical-info") +ranges.sort() +ranges.save() +``` + +Passe `resolve_ranges=False` à função `load()` para ignorar a deteção de componentes associados. + +## Meios de comunicação + +```python +for ref in lex.media_refs(): # todos os e + print(ref.kind, ref.href, ref.entry_id) + +lex.missing_media() # referências cujos ficheiros não existem +``` + +A resolução segue o esquema convencional: um link «href» relativo é verificado tal como fornecido (barras invertidas normalizadas — o WeSay escreve «pictures\photo with space.png») e na pasta «audio/» (para ficheiros de pronúncia) ou «pictures/» (para ilustrações). Os links «href» remotos/absolutos não podem ser verificados e são ignorados. + +## Outros conteúdos da pasta + +Uma pasta LIFT contém frequentemente ficheiros que o sil-lift não modela — o sistema de escrita LDML em `WritingSystems/`, os ficheiros de áudio/imagem relativos ao consentimento dos oradores do Combine em `consent/`, e outros semelhantes; As funções `load()`/`save()` não alteram estes ficheiros, e [`Lexicon.save_zip()`](lift-export-interop.md) transfere-os na íntegra ao empacotar a pasta. diff --git a/docs/pt/guides/large-files.md b/docs/pt/guides/large-files.md new file mode 100644 index 0000000..ca9bd44 --- /dev/null +++ b/docs/pt/guides/large-files.md @@ -0,0 +1,27 @@ +# Ficheiros de grande dimensão (transmissão em fluxo) + +A função `load()` constrói todo o grafo de objetos. No caso de léxicos com várias centenas de MB, a API de streaming processa uma entrada de cada vez numa memória limitada — o mesmo tipo `Entry`, pelo que o código escrito para um dos modos funciona também no outro. + +```python +import sil_lift + +with sil_lift.open_reader("big.lift") as reader: + header = reader.header # analisado antecipadamente (precede as entradas) + for entry in reader: # Iterador preguiçoso [Entry] + ... +``` + +```python +com sil_lift.open_reader("big.lift") como reader, sil_lift.open_writer( + "out.lift", header=reader.header, producer="my-script" +) como writer: + para cada entry em reader: + if not entry.date_deleted: # por exemplo, eliminar registos obsoletos + writer.write(entry) +``` + +Notas: + +- O resultado gerado pelo escritor é exatamente o que o serializador canónico de documento completo produziria para o mesmo conteúdo — os dois modos nunca divergem. +- O modo de transmissão contínua não reutiliza bytes da fonte: a saída é sempre canónica. Os resíduos LIFT ao nível da raiz — comentários entre entradas e atributos fora do esquema em `` — não são transportados; as entradas e o cabeçalho estão completos, incluindo os resíduos. +- Se ocorrer uma exceção no corpo de um bloco `open_writer`, o ficheiro fica visivelmente incompleto (sem o comando de fecho ``) — um léxico parcialmente escrito não deve parecer completo. diff --git a/docs/pt/guides/lift-export-interop.md b/docs/pt/guides/lift-export-interop.md new file mode 100644 index 0000000..104eecc --- /dev/null +++ b/docs/pt/guides/lift-export-interop.md @@ -0,0 +1,112 @@ +# Produção de LIFT em conformidade + +Este guia destina-se a quem estiver a desenvolver um _exportador_ LIFT — código em qualquer linguagem de programação que converta o modelo de dados de outra aplicação para o formato LIFT 0.13. O `sil-lift` desempenha duas funções nesse trabalho: uma verificação de conformidade que compara a saída com o esquema e com a semântica que o esquema não consegue expressar, e uma referência para as formas e regras de texto que a saída deve seguir. + +Escrever LIFT é muito mais fácil do que analisá-lo: um exportador apenas emite o subconjunto de construções que o seu próprio modelo produz e nunca se depara com todas as opções da especificação completa. A parte mais complicada são os detalhes — o complemento `.lift-ranges`, o texto específico para cada sistema de escrita, os identificadores estáveis e o escape de XML — e é precisamente isso que as verificações abaixo detetam. + +## Pacotes compactados + +O LIFT é normalmente transferido como um único ficheiro `.zip` — tanto o FieldWorks como o The Combine importam e exportam dessa forma — pelo que o `sil-lift` lê e grava pacotes compactados diretamente, independentemente do formato utilizado pelo ecossistema: os ficheiros na raiz do arquivo ou aninhados numa pasta de nível superior. + +- **Nota:** `sil_lift.load("package.zip")` extrai o conteúdo para um diretório temporário, localiza o único ficheiro `.lift` e carrega-o (os ficheiros complementares e os ficheiros multimédia são resolvidos como habitualmente). + - Os comandos da CLI `validate`, `stats`, `check-media` e `export` também aceitam um caminho `.zip`, pelo que o gate abaixo é executado diretamente sobre um pacote tal como está. + - `stats` e `export`, e extrair apenas o `.lift` em vez de todo o pacote — para que continuem a ser eficientes num pacote com muitos ficheiros multimédia, e para que o limite de extração se aplique apenas ao `.lift`, em vez de a tudo o que o rodeia. + - A extração está limitada a 10 GiB e 100 000 elementos; um pacote que exceda qualquer um desses limites é rejeitado com um erro `LiftParseError`, tal como acontece com um pacote cujos caminhos dos elementos estejam fora do diretório de extração. +- **Escreva:** `Lexicon.save_zip("out.zip", wrap_folder="MyDict")` compacta o ficheiro `.lift`, os seus `.lift-ranges` e todos os outros ficheiros da pasta de origem (multimédia, `WritingSystems/`, `consent/`, ...) num ficheiro zip. + - O `wrap_folder` tem, por predefinição, uma pasta de nível superior com o nome do ficheiro zip (conforme a convenção de importação do FieldWorks/Combine); passe `False` para obter um arquivo simples. + +Os ficheiros `.lift` e `.lift-ranges` mantêm a sua fidelidade ao nível do byte dentro do pacote; o próprio ficheiro zip não é reproduzível ao nível do byte. + +## Validar o resultado como um critério de conformidade + +Execute o comando `sil-lift validate` no ficheiro `.lift` gerado. Executa o RELAX NG (tanto no `.lift` como no seu complemento `.lift-ranges`) e realiza verificações semânticas que a gramática não consegue expressar: referências pendentes a `relation`/`variant`, GUIDs duplicados, integridade do elemento pai do intervalo, valores de características e de informação gramatical não definidos no respetivo intervalo e referências `range/@href` no cabeçalho que não remetem para nenhum elemento correspondente. + +No caso da CI, em caso de falha em qualquer etapa, gerar resultados legíveis por máquina: + +``` +sil-lift validate export.lift --strict --no-check-media --format json +``` + +- A opção `--strict` faz com que os avisos (e não apenas os erros) provoquem o falhanço da execução. +- `--no-check-media` ignora a verificação da presença de ficheiros multimédia no sistema de ficheiros, cujos resultados de `missing-media` são irrelevantes quando os ficheiros de áudio/fotografias não se encontram na mesma pasta que o ficheiro `.lift` na CI. +- `--format json` apresenta um único objeto JSON (`{"problems": [...], "summary": {...}}`) em vez de texto legível; os seus códigos de saída e esquema constituem uma interface suportada e abrangida pela SemVer (ver [o guia da linha de comandos](cli.md)). +- `--require-ids` também apresenta erros em entradas que não tenham um `guid` ou que não tenham um `id` — útil quando uma reimportação posterior tiver de atualizar, em vez de duplicar. + +Proteja-se contra a perda silenciosa de dados (o modo de falha que torna a exportação em CSV simples propensa a perdas) verificando as contagens com o comando `stats --format json` no seu modelo de origem: + +``` +sil-lift stats export.lift --format json +``` + +Apresenta as contagens de «entradas», «significados», «exemplos», «referências multimédia», «idiomas» e «características» por nome. + +### Executar o gate sem o conjunto de ferramentas do Python + +A integração contínua (CI) de um projeto em TypeScript ou C# pode executar a mesma verificação sem instalar o Python, através da GitHub Action incluída: + +```yaml +- utiliza: sillsdev/python-sil-lift@v0.1.0 + com: + caminho: export.lift + rigoroso: "true" + sem verificação de mídia: "true" + formato: json +``` + +ou a imagem do contentor, criada a partir do ficheiro `Dockerfile` do repositório: + +``` +docker build -t sil-lift . +docker run --rm -v "$PWD:/work" -w /work sil-lift validate export.lift --strict +``` + +## O componente complementar `.lift-ranges` + +Os vocabulários controlados — classes gramaticais, domínios semânticos e qualquer outro conjunto de valores baseado em características — encontram-se num ficheiro `.lift-ranges` paralelo, referenciado a partir do ficheiro `
`: + +```xml +
+ + + + +
+``` + +O guia contém a definição completa de cada gama. Os valores são ``s; `parent` cria uma hierarquia; `label` / `abbrev` / `description` são multitexts: + +```xml + + + + + +
n
+
+
+ + + + + +
+``` + +Uma entrada remete, então, para um valor através do seu identificador: a classe gramatical de um sentido é ``, e um domínio semântico é ``. O `sil-lift validate` emite um aviso (`undefined-range-value`) quando um valor não está definido no seu intervalo e um erro (`range-parent`) quando um `parent` não é um ID de elemento irmão — por isso, indique os intervalos que os seus dados utilizam efetivamente. Ver também [Intervalos e meios de comunicação](folder-media.md). + +Se criar a exportação em Python, as funções `Lexicon.add_ranges_file()`, `RangesFile.add_range()` e `Range.add_element()` criam o objeto complementar e adicionam as referências do cabeçalho automaticamente; `open_writer(..., ranges=...)` faz o mesmo no caminho de streaming. + +## Texto e multitexto + +Cada sequência de caracteres de uma língua humana no LIFT é um _multitexto_: um `
` por sistema de escrita, cada um envolvendo um ``: + +```xml + + kanga +
galinha
+
+``` + +Um modelo que indexa cadeias de caracteres pelo código do idioma (um `MultiString`, um `Record`, um `dict[str, str]`) estabelece uma correspondência um-para-um com este: cada entrada por chave corresponde a um `
`. É permitida, no máximo, uma forma por língua num único multitexto — caso contrário, o `sil-lift` emite um aviso de `duplicate-form-lang`. + +A codificação de escape em XML é a única parte em que a precisão é realmente fundamental. No texto dos elementos, os caracteres `&`, `<`, and `>` devem ser escapados (`&`, `<`, `>`); nos valores dos atributos, o caractere de aspas também deve ser escapado. O autor do `sil-lift` aplica exatamente estas regras e nunca altera os espaços em branco dentro de `` — não adiciona qualquer indentação nesse local, pois isso corromperia os dados lexicais. Se pretender obter o mesmo resultado, reutilize o processo de escape de um serializador XML verdadeiro (em vez de uma substituição feita manualmente que se esqueça do `&`) e mantenha o conteúdo de `` byte a byte, tal como aparece na sua fonte. diff --git a/docs/pt/guides/read-edit-write.md b/docs/pt/guides/read-edit-write.md new file mode 100644 index 0000000..d25e5b1 --- /dev/null +++ b/docs/pt/guides/read-edit-write.md @@ -0,0 +1,66 @@ +# Ler, editar, escrever + +## A carregar + +```python +import sil_lift + +lex = sil_lift.load("dictionary.lift") +``` + +A função `load()` aceita qualquer documento LIFT **0.13** bem formado — incluindo ficheiros reais que não estejam em conformidade com o esquema. Tudo o que o modelo não definir (elementos/atributos desconhecidos, comentários) é transportado sem perdas como resíduo LIFT no campo opaco `extra` de cada nó. Outras versões do LIFT provocam um `LiftParseError` indicando a versão. + +## O modelo + +Cada elemento do LIFT é uma classe de dados tipada: `Entry`, `Sense`, `Example`, `Pronunciation`, `Variant`, `Relation`, `Etymology`, `Reversal`, e assim por diante. Um texto multilingue é um `Multitext`, que funciona como um mapeamento de um código de idioma para `Text`: + +```python +entry = lex.find(id="abat") + +str(entry.lexical_unit["seh"]) # "abat" +entry.lexical_unit["en"] = "grove" # as cadeias de caracteres simples são convertidas +"en" in entry.citation # False +``` + +O `Text` está estruturado — uma lista ordenada de fragmentos `str` e `Span` — porque `` pode conter marcação aninhada ``. `str(text)` converte o texto em texto simples; os fragmentos mantêm a marcação para permitir a conversão de ida e volta. + +No LIFT, as glossas têm a forma de _Form_ (cada `` contém a sua própria linguagem), pelo que um sentido tem `glosses: list[Form]`, além de uma função auxiliar: + +```python +sense = entry.senses[0] +sense.gloss("en") # Texto | None +entry.gloss_langs() # {"en", "id"} +``` + +## Poupança + +```python +lex.save() # voltar para o local de onde foi carregado +lex.save("elsewhere.lift") +``` + +As entradas que não foram modificadas são gravadas de volta **identicamente, ao nível do byte**; um documento que não tenha sido modificado de todo é idêntico, ao nível do byte, desde o primeiro byte até ao último. Consulte [Garantias da Fidelity](../fidelity.md) para conhecer o contrato na íntegra. + +## Construir do zero + +```python +lex = sil_lift.Lexicon(producer="my-script 1.0") +entry = sil_lift.Entry(id="hello", guid="...") +entry.lexical_unit["en"] = "hello" +sense = sil_lift.Sense() +sense.glosses.append(sil_lift.Form("fr", sil_lift.Text(["bonjour"]))) +entry.senses.append(sense) +lex.entries.append(entry) +lex.save("new.lift") +``` + +## Ordenação canónica + +```python +lex.sort() # entradas por (guid, id); intervalos/definições de campos por id/tag +lex.save() # as entradas não alteradas mantêm os seus bytes exatos, na nova ordem + +sil_lift.canonicalize("in.lift", "out.lift") # totalmente resserializado, pronto para comparação de diferenças +``` + +Ver também: [Exemplo prático: edição em massa de glossários](bulk-edit-glosses.md). diff --git a/docs/pt/guides/validate.md b/docs/pt/guides/validate.md new file mode 100644 index 0000000..cb66815 --- /dev/null +++ b/docs/pt/guides/validate.md @@ -0,0 +1,36 @@ +# Validar + +A validação é sempre explícita — o carregamento e o guardamento nunca validam implicitamente. + +```python +import sil_lift + +# Exaustivo: um fluxo preguiçoso de Problemas (esquema + camadas semânticas). +for problem in sil_lift.iter_problems("dictionary.lift"): + print(problem) + # erro [dangling-ref] dictionary.lift:88 (entrada apu): a referência 'nope' corresponde a ... + +# Falha rápida: lança um LiftValidationError no primeiro problema de nível de erro. +sil_lift.validate_file("dictionary.lift") + +# Estado na memória (serializa primeiro — um custo documentado em léxicos de grande dimensão): +lex = sil_lift.load("dictionary.lift") +problems = list(lex.iter_problems()) +``` + +Cada `Problema` contém um `nível` (`"erro"`/`"aviso"`), um `código` fixo, uma `mensagem` e um endereço: `ficheiro`, `entry_id`, `guid`, `linha`. + +## As camadas + +1. **RELAX NG** em relação à gramática LIFT 0.13 (fornecida pelo lift-standard — uma cópia byte a byte incorporada neste pacote). +2. **Esquema de intervalos** — o ficheiro `lift-ranges-0.13.rng` deste projeto — sobre todos os companheiros `.lift-ranges` monitorizados. +3. **Verificações semânticas** que a gramática não consegue expressar: `duplicate-guid`, `dangling-ref`, `range-parent`, `undefined-range-value`, `duplicate-form-lang`, `missing-media`. + +## Resultados do FieldWorks (FLEx) em condições reais + +O FieldWorks cria sistematicamente algum conteúdo que as ferramentas de verificação rigorosas rejeitam. Eis a política do sil-lift, para que os léxicos reais sejam validados de forma útil: + +- Os hrefs do tipo `file://C:/...` (URIs inválidas) são assinalados como **avisos** (`uri-not-rfc`), e não como erros de esquema — o validador C# nunca os rejeitou. +- Os elementos filhos intercalados legalmente (por exemplo, `field, note, field, note`, de certa forma) **não** são assinalados, o que permite contornar um falso positivo na libxml2. +- Os valores dos intervalos são comparados de acordo com a normalização NFC do Unicode — o FLEx grava o ficheiro `.lift` em NFC, mas o ficheiro `.lift-ranges` em NFD, dentro da mesma exportação. +- As extensões `trait`/`field` do FLEx dentro de `range-element` **são** assinaladas (erros de esquema em relação ao esquema de intervalos): tratam-se de verdadeiros desvios em relação à especificação. diff --git a/docs/pt/index.md b/docs/pt/index.md new file mode 100644 index 0000000..8620e4d --- /dev/null +++ b/docs/pt/index.md @@ -0,0 +1,32 @@ +# sil-lift + +Uma biblioteca Python para o [LIFT](https://github.com/sillsdev/lift-standard) (Lexicon Interchange FormaT) 0.13: leitura e gravação sem perdas da pasta LIFT (`.lift` + `.lift-ranges` + referências de multimédia), validação do esquema e semântica, e ordenação canónica — com APIs de streaming para léxicos de grande dimensão. + +**Estado: pré-lançamento, em desenvolvimento ativo.** + +## Instalar + +Extraído do [PyPI](https://pypi.org/project/sil-lift/): + +``` +pip install sil-lift # biblioteca + o comando sil-lift +``` + +Requer o Python 3.11 ou superior. A única dependência de execução é o lxml. + +## A visita guiada de 30 segundos + +```python +import sil_lift + +lex = sil_lift.load("thesaurus.lift") # também rastreia os companheiros dos intervalos .lift + +for entry in lex.entries: + if "en" not in entry.gloss_langs(): + print(entry.id, str(entry.lexical_unit.get("seh") or "")) + +entry = lex.find(guid="0f5a9c3e-...") # ou lex.find(id="hoofd_a1b2") +entry.senses[0].definition["en"] = "head (anatomy)" + +lex.save() # as entradas não alteradas mantêm-se idênticas em termos de bytes; a entrada editada é novamente serializada +``` diff --git a/docs/pt/reference.md b/docs/pt/reference.md new file mode 100644 index 0000000..de8544b --- /dev/null +++ b/docs/pt/reference.md @@ -0,0 +1,9 @@ +# Referência da API + +A API pública é exatamente o que o `sil_lift` reexporta ao nível superior; os módulos com o prefixo «_» são detalhes de implementação. + +::: sil_lift +options: +show_root_heading: false +show_source: false +members_order: source diff --git a/docs/ru/csharp-differences.md b/docs/ru/csharp-differences.md new file mode 100644 index 0000000..745826c --- /dev/null +++ b/docs/ru/csharp-differences.md @@ -0,0 +1,52 @@ +# Отличия от библиотек C\# + +sil-lift в общих чертах аналогичен инструментарию SIL для C# LIFT — в первую очередь `SIL.Lift` из [libpalaso](https://github.com/sillsdev/libpalaso) (парсер, валидатор, мигратор, `LiftSorter`), `SIL.DictionaryServices` в том же репозитории (модель `LexEntry`/`LexSense` с собственным считывателем/записывателем LIFT, которую используют The Combine и WeSay), а также обработчики LIFT в [Chorus](https://github.com/sillsdev/chorus). Это новая реализация, а не порт. На этой странице приведены краткие сведения о том, в каких случаях поведение намеренно отличается. + +## Область применения + +| Возможности | Библиотеки C# | sil-lift | +| ----------------------------------- | --------------------------------------------------------------------- | --------------------------------------------------------------------------------------- | +| Версии LIFT | 0,10–0,13 (с учётом миграции) | **Только версия 0.13**; более старые версии отклоняются с явной ошибкой | +| Переход на новую версию | `Migrator` (цепочка XSLT) | нет — использовать XSLT-файлы из пакета lift-standard для разовых обновлений | +| 3-стороннее слияние / синхронизация | Припев | выходит за рамки | +| Валидация | Только RELAX NG (`Validator`) | RELAX NG + проверки схемы и семантики | +| Потоковое вещание | внутренний синтаксический анализ с гранулярностью на уровне элементов | публичный API `open_reader` / `open_writer` | + +## Формат API + +Парсер `SIL.Lift` работает на основе обратных вызовов (`ILexiconMerger`): он передаёт события разбора потребителю. Вместо этого sil-lift возвращает простой граф объектов — типизированные классы данных для каждого элемента LIFT — поскольку разработчики скриптов на Python нуждаются в объектах, а не в обратных вызовах. `SIL.DictionaryServices` действительно накладывает объектную модель `LexEntry`/`LexSense` на `SIL.Lift`, однако как прикладная модель она представляет только те конструкции, которые используют эти приложения — поэтому повторная сериализация через неё не может сохранить контент, выходящий за пределы модели, так же, как это делают обработка остатков LIFT и точность на уровне байтов в sil-lift (см. ниже). API потоковой обработки возвращает _тот же самый_ тип `Entry`, поэтому нет необходимости осваивать вторую, упрощённую модель. + +## Точность воспроизведения в обоих направлениях + +Самое явное намеренное отличие. При сохранении с помощью `SIL.Lift` происходит повторная сериализация всего документа. Компания «Sil-Lift» гарантирует: + +- неизменённый документ сохраняется **с идентичным количеством байтов**, и +- неизмененные записи сохраняют свои исходные байты в точности, даже если другие записи изменяются — это тот же принцип разбиения на байтовые блоки для каждой записи, который использует Chorus, и который применяется автоматически. + +См. [Гарантии Fidelity](fidelity.md). + +## Валидация + +Валидатор `Validator` на языке C# выполняет один проход по RELAX NG и возвращает первые ошибки в виде строк. sil-lift передаёт структурированный поток `Problem`, каждый элемент которого содержит информацию о файле, записи и строке, к которым он относится, при этом его схема намеренно отличается в трёх местах: + +- **Недопустимые URI являются предупреждениями, а не ошибками.** Движок C# RELAX NG никогда не обеспечивал соблюдение типа данных `anyURI`, поэтому FieldWorks (FLEx) на протяжении многих лет записывает ссылки `file://C:/...` в реальные лексиконы. Отказ от этих файлов привёл бы к отметке практически каждого экспорта из FLEx. +- **Применяются правила Schematron** (в качестве семантических проверок): дублирующиеся языки форм и аналогичные совместные ограничения в грамматике LIFT без предупреждения игнорировались как при валидации на C#, так и при валидации с помощью исходного lxml. +- **Сравнения между файлами нормализованы в соответствии со стандартом Unicode**, поскольку FLEx записывает файл `.lift` в формате NFC, а сопутствующий файл `.lift-ranges` — в формате NFD. + +sil-lift также проверяет соответствие файлов `.lift-ranges`, сопутствующих загруженному лексикону, схеме для автономных документов диапазонов (поставляемой из `lift-standard` вместе с базовой грамматикой LIFT) — каждый отслеживаемый внешний файл диапазонов проверяется при каждой валидации `.lift` — в то время как в среде C# такой схемы (или проверки) нет. (Не существует отдельной точки входа для проверки файла `.lift-ranges`, не связанного с файлом `.lift`.) + +## Каноническая сортировка + +`Lexicon.sort()` повторяет основные правила `LiftSorter` (записи сортируются по GUID без учета регистра; диапазоны и элементы диапазонов — по ID; определения полей заголовка — по тегам; значения слов сохраняются в том же порядке, что и в файле; пробелы внутри `` никогда не изменяются), за исключением трёх отличий: + +- записи без GUID сортируются детерминированно по ID (класс `LiftSorter` предполагает наличие GUID); +- порядок сортировки не зависит от локали (используются кодовые точки без учета регистра, а не сортировка с инвариантной культурой в .NET); +- Списки одного типа, такие как заметки, связи и формы, сохраняют порядок, в котором они были в документе, а не пересортировываются по ключу — группировка и так является детерминированной, а их переупорядочение лишь увеличивает объем данных сравнения. + +Файл `canonicalizeLift.xsl` из репозитория спецификаций вообще не используется: он сжимает пробелы внутри лексического текста (деструктивно), а сгенерированные им идентификаторы при каждом запуске отличаются друг от друга. + +## Не перенесено + +- Удобства, характерные для WeSay (работа с панелью управления и настройками, связанными с файлами LIFT). +- `SynchronicMerger` (объединение обновлений Chorus) — идея разбиения на байтовые блоки сохраняется в уровне точности, а сам процесс объединения — нет. +- Анализ систем письма LDML: файлы в папке `WritingSystems/` рассматриваются как непрозрачное содержимое папки. diff --git a/docs/ru/fidelity.md b/docs/ru/fidelity.md new file mode 100644 index 0000000..9069ff1 --- /dev/null +++ b/docs/ru/fidelity.md @@ -0,0 +1,33 @@ +# Гарантии Fidelity + +LIFT — это _промежуточный_ формат, поэтому первое правило гласит: **никогда не удаляйте то, чего не понимаете**. Контракт `sil-lift`, проверка которого осуществляется набором тестов при каждом запуске (файлы корпуса плюс генерация на основе свойств): + +## Чтение + +Загружается любой корректно сформированный документ LIFT 0.13 — включая контент, не соответствующий схеме. Все, что не определено в модели, передается в непрозрачный контейнер `Extras` ближайшего узла в качестве _остатка LIFT_ — так в FieldWorks называется аналогичная концепция, которая хранится в поле `LiftResidue`: неизвестные атрибуты и элементы, комментарии XML и инструкции по обработке, случайный текст, а также некорректно сформированные типизированные атрибуты (неправильная дата сохраняется в виде исходной строки в `Extras`; типизированное поле имеет значение `None`). + +## Сохранение документа без изменений + +`load()` → `save()` без изменений записывает **байтово идентичный вывод** — без переформатирования, без повторного экранирования, без изменения порядка, с сохранением маркеров порядка байтов и деклараций XML. В настоящее время список нормализации отсутствует: тождество является точным. + +Исключения (автор переходит к полной канонической сериализации, которая является семантически полной, но не сохраняет байтовую структуру): + +- кодировка исходного текста не совместима с ASCII (не UTF-8/US-ASCII), или +- источник содержит DOCTYPE, либо +- сканер байтов и синтаксический анализатор не сходятся во мнениях относительно структуры верхнего уровня документа — например, в случае второго символа `
`, не предусмотренного спецификацией, который синтаксический анализатор сохраняет только один раз (сканер действует намеренно консервативно: при любом сомнении он не фиксирует исходные байты вообще), или +- исходный код был скомпилирован в памяти, а не загружен из файла. + +## Сохранение отредактированного документа + +- **Неизмененные записи выводятся дословно из исходных байтов.** Запись считается измененной, если какая-либо часть её объекта модели изменилась с момента синтаксического анализа (что определяется по моментальному снимку канонической сериализации, а не по флагу «dirty»). +- **Измененные записи повторно сериализуются канонически и полностью**: UTF-8, отступ в 2 пробела _вне_ смешанного контента (пробелы внутри `` и `` никогда не изменяются), задокументированная группировка дочерних элементов для каждой записи (например, запись: лексическая единица, цитата, варианты произношения, варианты, значения, примечания, связи, этимологии, аннотации, характеристики, поля), фиксированный порядок атрибутов, даты в формате ISO-8601 (`Z` для UTC). Все оставшиеся элементы повторно выводятся; их позиции восстанавливаются в исходном списке дочерних элементов, привязываясь к новому списку дочерних элементов (это приблизительное значение — точные позиции в байтах гарантируются только для нетронутых записей). +- Добавление, удаление или изменение порядка записей приводит к повторной сериализации структуры документа, однако байты каждой неизменённой записи по-прежнему выводятся дословно. + +!!! note ""Канонический XML-файл" , приведенный здесь, не связан с каким-либо другим каноническим XML-файлом" + Под «канонической формой» на этой странице понимается собственная, задокументированная форма `sil-lift`, описанная в одном из пунктов выше. Это не имеет отношения к процессу W3C по стандарту Canonical XML (C14N). Это не имеет отношения к классу `CanonicalXmlSettings` из `SIL.Core`. + +## Известные приближения (только затронутые узлы) + +- Комментарии, расположенные _внутри_ блока ``, сохраняются, но перемещаются рядом с этим блоком, а не остаются на прежнем месте с точным смещением по символам. +- Крестообразный порядок дочерних элементов внутри редактируемого элемента приводится в соответствие с канонической группировкой (параметр `interleave` схемы LIFT делает этот порядок семантически несущественным). +- Элемент «multitext», который присутствует, но не содержит ничего — ни форм, ни остаточных элементов, например, ``, — не повторно выводится. Модель представляет эти поля в виде всегда присутствующего объекта `Multitext` (`lexical-unit`, `citation`, `definition`, `usage` отношения и `label` / `abbrev` / `description` для ссылок на URL, диапазонов, элементов диапазона и заголовка), поэтому пустое поле после синтаксического анализа неотличимо от отсутствующего. Ничего из семантики не теряется. diff --git a/docs/ru/guides/build-export.md b/docs/ru/guides/build-export.md new file mode 100644 index 0000000..9ec97a6 --- /dev/null +++ b/docs/ru/guides/build-export.md @@ -0,0 +1,168 @@ +# Пример с расчетами: создание экспорта LIFT с нуля + +Если вы экспортируете данные другого приложения в формате LIFT — что и является целью задачи [Создание LIFT-документов, соответствующих стандарту](lift-export-interop.md) — `sil-lift` может сформировать документ объект за объектом и сериализовать его, вместо того чтобы вручную генерировать XML. Здесь подробно описан один скрипт, который формирует запись, содержащую все элементы, присущие реальному словарю (несколько систем письма, произношение, значение с примером, иллюстрацию, характеристику семантической области и поле, специфичное для приложения), записывает контролируемые словари в сопутствующий файл `.lift-ranges`, проверяет их на корректность и сохраняет. + +## Сценарий + +```python +from pathlib import Path + +import sil_lift + +lex = sil_lift.Lexicon(producer="my-exporter") + +# Одна запись, построенная на основе исходной модели. +entry = sil_lift.Entry(id="kanga", guid="6b9e7c2a-3f4d-4a1b-8c5e-2d9f0a1b2c3d") +entry.lexical_unit["seh"] = "nkhuku" +entry.lexical_unit["pt"] = "galinha" + +pron = sil_lift.Pronunciation() +pron.forms["en"] = "Speaker: Ana" # Конвенция Combine для обозначения говорящего +pron.media.append(sil_lift.URLRef(href="audio/nkhuku.wav")) +entry.pronunciations.append(pron) + +sense = sil_lift.Sense(id="kanga_s1") +sense.grammatical_info = sil_lift.GrammaticalInfo(value="Noun") +sense.glosses.append(sil_lift.Form(lang="en", text=sil_lift.Text(["chicken"]))) +sense.definition["en"] = "a domestic fowl kept for its eggs and meat" + +example = sil_lift.Example() +example.forms["seh"] = "Ndinafuna nkhuku." +translation = sil_lift.Translation() +translation.forms["en"] = "I want a chicken." +example.translations.append(translation) +sense.examples.append(example) + +photo = sil_lift.URLRef(href="pictures/hen.jpg") +photo.label["en"] = "Курица" +sense.illustrations.append(photo) + +sense.traits.append(sil_lift.Trait(name="semantic-domain-ddp4", value="1.6.1.2")) + +scientific = sil_lift.Field(type="scientific-name") # дополнительное поле, специфичное для приложения +scientific.content["en"] = "Gallus gallus domesticus" +sense.fields.append(scientific) + +entry.senses.append(sense) +lex.entries.append(entry) + +# Контролируемые словари, на которые ссылается запись, в сопутствующем файле .lift-ranges. +ranges = sil_lift.RangesFile() +ranges.add_range("grammatical-info").add_element("Noun").label["en"] = "noun" +ranges.add_range("semantic-domain-ddp4").add_element("1.6.1.2").label["en"] = "Bird" +lex.add_ranges_file(ranges, href="birds.lift-ranges") + +# Проверить, что запишет функция save(), прежде чем записывать на диск. +problems = list(lex.iter_problems()) +print(f"validation: {len(problems)} проблема(ы)") + +out = Path("export") +out.mkdir(exist_ok=True) +lex.save(out / "birds.lift") +print("=== birds.lift ===") +print((out / "birds.lift").read_text(encoding="utf-8"), end="") +print("=== birds.lift-ranges ===") +print((out / "birds.lift-ranges").read_text(encoding="utf-8"), end="") +``` + +## Что он производит + +`validation: 0 problem(s)`, а затем `.lift` и его сопутствующий код рядом: + +``` +=== birds.lift === + + +
+ + + + +
+ + + + nkhuku + +
+ galinha +
+
+ +
+ Speaker: Ana +
+ +
+ + + + chicken + + +
+ a domestic fowl kept for its eggs and meat +
+
+ +
+ Ndinafuna nkhuku. +
+ +
+ I want a chicken. +
+
+
+ + + + + +
+ Gallus gallus domesticus +
+
+
+
+
+=== birds.lift-ranges === + + + + + + + + + + + + + +``` + +## Примечания по API + +- Поля с несколькими текстами (`lexical_unit`, `definition`, метка `Form`/`URLRef`, содержимое `Field`, ...) Через интерфейс сопоставления передать по одной строке для каждой системы письма: `entry.lexical_unit["seh"] = "nkhuku"` добавляет `
`. Модель исходных данных, в которой строки индексируются по коду языка, напрямую соответствует этой схеме. +- `RangesFile.add_range()` / `Range.add_element()` формируют контролируемые словари, а `Lexicon.add_ranges_file(ranges, href=...)` присоединяет сопутствующий файл и добавляет ссылки в заголовок `` — таким образом, ссылки `` и `` в записи разрешаются в соответствии с заданными вами диапазонами. +- `URLRef` — это атрибут `href` с дополнительным многострочным текстом подписи или метки (необязательным) — используется как для `` (аудио), так и для `` (фотографии). Произношение здесь соответствует принятой в «The Combine» конвенции, согласно которой форма «en» читается как «Speaker: ». +- Данные, специфичные для приложения, не содержащие данных о поездках домой по системе LIFT, в формате `` (или ``): FieldWorks интерпретирует их как пользовательские поля, а The Combine сохраняет их. +- Присваивайте каждой записи уникальный и стабильный `guid` (например, с помощью `uuid.uuid4()`, который используется повторно при всех экспортах) — при последующем повторном импорте запись будет обновлена на месте, а не дублирована. Команда `sil-lift validate --require-ids` обеспечивает соблюдение этого требования. +- Функция `lex.iter_problems()` проверяет документ, хранящийся в памяти (то, что записала бы функция `save()`), прежде чем какие-либо данные поступят на диск; в данном случае он находится в исправном состоянии. Поскольку для лексикона пока не создана папка, проверки наличия медиафайлов и ссылок на сопутствующие ресурсы пропускаются — запустите команду [`sil-lift validate`](cli.md) на сохраненном выводе (или с параметром `--no-check-media`), как только аудио- и фотофайлы будут размещены в соответствующих папках. + +## Упаковка + +Команда `lex.save("export/birds.lift")` записывает папку в формате (файлы `.lift` и `.lift-ranges`, расположенные рядом). Чтобы сгенерировать единый ZIP-архив, который программы FieldWorks и The Combine могут импортировать напрямую, вместо этого используйте `lex.save_zip("birds.zip")` — см. [Создание LIFT-файлов, соответствующих стандарту](lift-export-interop.md). diff --git a/docs/ru/guides/bulk-edit-glosses.md b/docs/ru/guides/bulk-edit-glosses.md new file mode 100644 index 0000000..16ad85a --- /dev/null +++ b/docs/ru/guides/bulk-edit-glosses.md @@ -0,0 +1,79 @@ +# Пример с решением: массовое редактирование глосс + +Типичная задача по обслуживанию: привести правописание всех английских терминов в словаре к единому стандарту (британский → американский или наоборот), не затрагивая при этом остальные элементы файла. В этом примере подробно рассматривается один скрипт, который выполняет загрузку, редактирование, проверку и сохранение данных, демонстрируя совместную работу API редактирования и гарантии точности. + +## Сценарий + +```python +import sys + +import sil_lift + +path = "dictionary.lift" +lex = sil_lift.load(path) + + +def iter_senses(senses): + """Выдает все значения, включая подзначения (рекурсивно).""" + for sense in senses: + yield sense + yield from iter_senses(sense.subsenses) + + +edited_glosses = 0 + +for entry in lex.entries: + for sense in iter_senses(entry.senses): + for gloss in sense.glosses: + if gloss.lang != "en": + continue + old = str(gloss.text) + new = old.replace("colour", "color") + if new != old: + gloss.text = sil_lift.Text([new]) + edited_glosses += 1 + +changed = lex.changed_entries() + +errors = [p for p in lex.iter_problems() if p.level == "error"] +if errors: + for problem in errors: + print(problem) + sys.exit(f"прерывание: {len(errors)} ошибка(и) проверки, ничего не сохранено") + +lex.save() +print(f"отредактировано {edited_glosses} глосса(рий) в {len(changed)} записи(ях)") +``` + +Несколько моментов, на которые стоит обратить внимание: + +- `Sense.subsenses` само по себе представляет собой `list[Sense]`, поэтому `iter_senses` выполняет рекурсию по нему — при массовом редактировании, которое бы просматривало только `entry.senses`, любые глоссы, вложенные под подзначениями, были бы незаметно пропущены. +- `gloss.text` — это объект типа `Text`, а не обычная строка: функция `str(gloss.text)` преобразует его в строку для сопоставления, а результат замены записывается обратно с помощью `sil_lift.Text([new])`, а не путем изменения исходной строки на месте. +- Функция `lex.changed_entries()` выводит список записей, которые отличаются от содержимого загруженного файла. Поскольку дайджест записи охватывает всё её поддерево, при внесении изменений во вложенное подзначение об этом получает уведомление запись, в которой оно содержится. + - Он сравнивает сериализованные данные, поэтому присвоение полю значения, которое у него уже было, не фиксируется. + - Она сообщает только об изменениях в содержимом; функции `lex.added_entries()` и `lex.removed_entries()` охватывают записи, которые появились или исчезли с момента загрузки. + - Он возвращает сами записи, при этом не учитывая дубликаты или отсутствие `id` (что допускается в LIFT). + - В качестве показателя это имеет смысл только в том случае, если есть с чем сравнивать. Когда байтовый сканер отказывается считывать исходный код — из-за кодировки, несовместимой с ASCII, или из-за несогласованности между сканером и парсером — базовая линия отсутствует, и функция `changed_entries()` сообщает о _каждой_ записи. Это честный ответ в отношении защиты от записи, поскольку в данном случае функция `save()` повторно сериализует весь файл, но это означает, что подсчитанное значение отражает размер лексикона, а не размер внесенных изменений. +- Функция `lex.changes()` сообщает, изменился ли документ _хотя бы немного_. Он охватывает не только записи, но и заголовок, корневой элемент, а также все элементы с классом `.lift-ranges`. + - Это будет ложным только в том случае, если `save()` воспроизводит исходные байты, что делает `if not lex.changes(): ...` правильным способом пропустить ненужную запись. Гарантия действует в одном направлении: она никогда не возвращает сообщение «нет данных для записи» для документа, который подлежит перезаписи, в то время как изменение, вызывающее полную повторную сериализацию, может привести к возврату исходных байтов и все равно будет отражено в отчете. + - Он сравнивает содержимое, а не место назначения, поэтому используйте его только для сохранения на месте: `lex.save(some_other_dir / "dictionary.lift")` записывает документ и сопутствующие файлы в пустое место, независимо от того, были ли внесены изменения. + - Это защитная мера, а не ускорение — при обработке этого вызова анализируется каждая запись, что соответствует той же работе, которую выполняет `save()` для определения, какие исходные байты можно повторно использовать. Таким образом, вы пропускаете именно саму запись (время изменения файла не изменяется, не возникает ложных различий), а не процесс принятия решения. +- При проверке в памяти (`lex.iter_problems()`) сначала выполняется сериализация отредактированного состояния, благодаря чему оно правильно отражает внесенные изменения ещё до записи на диск. Прерывание при возникновении любого объекта `Problem` уровня `"error"` — предупреждения оставляются на усмотрение вызывающего модуля — означает, что некорректное изменение никогда не доходит до вызова `save()`. + +Не только глянцевые поверхности стоит обрабатывать таким образом. Та же самая поверхность сопоставления `Multitext` применяется к определениям и всем другим многоязычным полям в записи или значении: + +```python +sense.definition["en"] = "цвет предмета" +``` + +## Запуск программы + +Проведите сравнение с небольшим лексиконом, в котором есть термин и его подзначение, обозначающие «цвет»: + +``` +внесено 2 исправления в 1 запись +``` + +## Выгода от точности воспроизведения + +Гарантия распространяется на каждую отдельную запись: запись, модель которой не изменилась, возвращается в виде **байтово идентичного** копирования исходного содержимого, а повторная сериализация производится только для тех записей, в которые вы действительно вносили изменения. В приведенном выше фрагменте в одной записи были отредактированы глоссы — все остальные записи в файле сохранили свои байты без изменений. (Обратите внимание на степень детализации: при редактировании любой части записи происходит повторная сериализация всей записи целиком, включая нетронутые родственные значения.) Таким образом, при редактировании одного глоссария в словаре, содержащем 50 000 статей, создается файл diff, затрагивающий одну статью, а не переформатированный файл. Точные условия договора см. в разделе [Гарантии Fidelity](../fidelity.md). diff --git a/docs/ru/guides/cli.md b/docs/ru/guides/cli.md new file mode 100644 index 0000000..2fadf45 --- /dev/null +++ b/docs/ru/guides/cli.md @@ -0,0 +1,70 @@ +# Командная строка + +При установке пакета (`pip install sil-lift`) также устанавливается команда `sil-lift` — поддерживаемый инструмент в духе LiftTools, входящий в состав пакета (а также, в случае команды `validate`, готовый пример использования API библиотеки). + +``` +sil-lift validate PATH [--format {text,json}] [--strict] [--no-check-media] [--require-ids] + все проблемы с указанием файла/записи/строки; выход с кодом 1 при ошибках +sil-lift stats PATH [--format {text,json}] + подсчёт записей/значений/языков (потоково; любой размер) +sil-lift sort PATH [-o OUT] канонически отсортированная копия, готовая к сравнению (по умолчанию: на месте) +sil-lift check-media PATH отчёт об отсутствующих и осиротевших медиафайлах; завершение с кодом 1 при отсутствии +sil-lift export PATH [-o OUT] [--langs L] [--tsv] + одна строка на каждый листьевой сенс (подсенсы сглажены) в формат CSV/TSV (потоковая передача) +``` + +`--format json` выводит в stdout один объект JSON (и ничего больше) для использования в системах CI/автоматизации; схему см. в примере ниже. Параметр `--strict` рассматривает предупреждения как ошибки и возвращает код завершения 1 при обнаружении хотя бы одного из них — используйте его, чтобы остановить сборку при наличии даже одного предупреждения, а не только при ошибках. `--no-check-media` пропускает проверку наличия медиафайлов в файловой системе (подавляя появление сообщений об ошибках `missing-media`), что полезно при проверке только что сгенерированного экспорта, аудио- и фотофайлы которого находятся не в той же папке, а в другом месте. `--require-ids` дополнительно возвращает ошибку (`missing-id`) при обнаружении любой записи, в которой отсутствует `guid`, или сенса, в котором отсутствует `id` — это более строгий подход, чем в LIFT, и предназначен для рабочих процессов, в которых повторный импорт осуществляется по стабильному идентификатору. Если в качестве пути передать символ `-`, документ будет считываться из стандартного ввода (документ, передаваемый по конвейеру, не имеет папки, поэтому сопутствующий файл `.lift-ranges` и медиафайлы не определяются). Команда `stats` также поддерживает опцию `--format json`, выводя результаты подсчёта в виде одного объекта JSON. + +!!! note + Коды завершения функции `validate` и схема `--format json` представляют собой поддерживаемый интерфейс автоматизации: оба элемента проходят тестирование и изменяются только в соответствии с SemVer. + +`sort` перезаписывает только файл `.lift`; сопутствующие файлы `.lift-ranges` остаются неизменными +(их следует сортировать отдельно с помощью API `RangesFile`). + +Команды `validate`, `stats`, `check-media` и `export` также поддерживают заархивированный пакет LIFT (файл с расширением `.zip` в любом из двух вариантов структуры — с файлами в корневом каталоге архива или вложенными в одну папку верхнего уровня); он распаковывается во временный каталог и удаляется по завершении выполнения команды. Команды потоковой обработки `stats` и `export` извлекают только сам файл `.lift`, поэтому их выполнение не требует значительных ресурсов даже для пакетов с большим объемом мультимедиа; команды `validate` и `check-media` требуют доступ к всей папке и извлекают все её содержимое. + +Примеры: + +``` +$ sil-lift validate dictionary.lift +ошибка [dangling-ref] dictionary.lift:88 (запись apu): ссылка «nope» не соответствует ни одному идентификатору записи (ID/GUID), ни идентификатору значения (sense ID) +предупреждение [uri-not-rfc] dictionary.lift:6: : Буква диска Windows использована в качестве авторитета URI (file://C:/ в стиле FLEx) +1 ошибка, 1 предупреждение + +$ sil-lift validate dictionary.lift --format json +{ + "problems": [ + { + "level": "error", + "code": "dangling-ref", + "message": "ref 'nope' не соответствует ни одному id/guid записи или id значения", + "file": "dictionary.lift", + "entry_id": "apu", + "guid": null, + "line": 88 + }, + { + "level": "warning", + "code": "uri-not-rfc", + "message": ": В качестве авторитета URI использована буква диска Windows (file://C:/ в стиле FLEx)", + "file": "dictionary.lift", + "entry_id": null, + "guid": null, + "line": 6 + } + ], + "summary": { + "errors": 1, + "warnings": 1 + } +} + +$ sil-lift stats sango.lift +записей: 3507 +значений: 4541 +... + +$ sil-lift export dictionary.lift --langs en,fr -o dictionary.csv +``` + +Коды завершения: `0` — операция выполнена успешно (предупреждения допускаются, за исключением случая, когда задан параметр `--strict`), `1` — обнаружены проблемы (ошибки валидации / отсутствующие носители / предупреждения при использовании параметра `--strict`), `2` — входные данные нечитаемы. diff --git a/docs/ru/guides/folder-media.md b/docs/ru/guides/folder-media.md new file mode 100644 index 0000000..37fc076 --- /dev/null +++ b/docs/ru/guides/folder-media.md @@ -0,0 +1,41 @@ +# Папка LIFT: диапазоны и носители + +Лексикон LIFT обычно представляет собой _папку_: файл `.lift`, один или несколько сопутствующих файлов `.lift-ranges` (файлы-«сидкары») и мультимедийные файлы в папках `audio/` и `pictures/`. + +## Диапазоны + +```python +lex = sil_lift.load("dictionary.lift") # сопутствующие файлы отслеживаются автоматически + +lex.ranges_files # {Path(...): RangesFile} +lex.all_ranges() # объединенное представление {id: Range} +lex.all_ranges()["grammatical-info"].elements +``` + +Механизм обнаружения сопутствующих файлов учитывает особенности реального мира: используется атрибут `range/@href`, указывающий на существующий файл; незавершенные абсолютные ссылки `file://C:/...` в FieldWorks перенаправляются на базовое имя ссылки рядом с расширением `.lift`; а стандартный соседний файл `.lift-ranges` подбирается даже в том случае, если на него ничто не ссылается. + +Функция `lex.save()` записывает файл `.lift` и все отслеживаемые сопутствующие файлы одновременно. Изменения, внесенные в файл `RangesFile`, сохраняются обратно в _этот_ файл; неизмененные диапазоны сохраняют свои точные байты. Использование в автономном режиме: + +```python +ranges = sil_lift.RangesFile.load("dictionary.lift-ranges") +ranges.find("grammatical-info") +ranges.sort() +ranges.save() +``` + +Передайте параметр `resolve_ranges=False` в функцию `load()`, чтобы пропустить поиск сопутствующих объектов. + +## СМИ + +```python +for ref in lex.media_refs(): # все ссылки типа « » и « + » print(ref.kind, ref.href, ref.entry_id) + +lex.missing_media() # ссылки, файлы которых отсутствуют +``` + +Определение следует стандартной схеме: относительный ссылочный адрес проверяется в том виде, в котором он указан (обратные косые черты нормализуются — WeSay записывает `pictures\photo with space.png`), и находится в каталоге `audio/` (для аудиофайлов с произношением) или `pictures/` (для иллюстраций). Удаленные/абсолютные ссылки не поддаются проверке и пропускаются. + +## Другое содержимое папки + +В папке LIFT часто хранятся файлы, которые sil-lift не моделирует — LDML для систем письма в каталоге `WritingSystems/`, аудио- и изобразительные файлы с согласием говорящих из проекта «The Combine» в каталоге `consent/` и т. п.; Функции `load()`/`save()` оставляют их без изменений, а [`Lexicon.save_zip()`](lift-export-interop.md) переносит их без изменений при упаковке папки. diff --git a/docs/ru/guides/large-files.md b/docs/ru/guides/large-files.md new file mode 100644 index 0000000..31b9ced --- /dev/null +++ b/docs/ru/guides/large-files.md @@ -0,0 +1,27 @@ +# Крупные файлы (потоковая передача) + +Функция `load()` создаёт весь граф объектов. В случае лексиконов объёмом в несколько сотен МБ потоковый API обрабатывает по одной записи за раз в ограниченном объёме памяти — при этом используется тот же тип `Entry`, поэтому код, написанный для одного режима, работает и в другом. + +```python +import sil_lift + +with sil_lift.open_reader("big.lift") as reader: + header = reader.header # проанализировано заранее (расположено перед записями) + for entry in reader: # ленивый итератор Iterator[Entry] + ... +``` + +```python +with sil_lift.open_reader("big.lift") as reader, sil_lift.open_writer( + "out.lift", header=reader.header, producer="my-script" +) as writer: + for entry in reader: + if not entry.date_deleted: # например, удалить «могильные камни» + writer.write(entry) +``` + +Примечания: + +- Результат работы этого модуля полностью соответствует тому, что выдал бы канонический сериализатор полного документа для того же самого контента — эти два режима никогда не расходятся. +- В режиме потоковой передачи байты исходных данных не используются повторно: выходные данные всегда являются каноническими. Остаточные данные LIFT на корневом уровне — комментарии между записями и атрибуты, выходящие за рамки схемы, на `` — не передаются; записи и заголовок являются полными, включая остаточные данные. +- Если в теле блока `open_writer` происходит исключение, файл остается видимо незавершенным (без закрывающего ``) — частично записанный лексикон не должен выглядеть завершенным. diff --git a/docs/ru/guides/lift-export-interop.md b/docs/ru/guides/lift-export-interop.md new file mode 100644 index 0000000..d921047 --- /dev/null +++ b/docs/ru/guides/lift-export-interop.md @@ -0,0 +1,112 @@ +# Создание LIFT, соответствующего требованиям + +Данное руководство предназначено для всех, кто разрабатывает _экспортер_ LIFT — программу на любом языке программирования, преобразующую модель данных другого приложения в формат LIFT 0.13. `sil-lift` выполняет в этой работе две функции: служит механизмом проверки соответствия, который сравнивает выходные данные со схемой и учитывает семантику, которую схема не может выразить, а также выступает в качестве эталона для форм и правил оформления текста, которым должны соответствовать выходные данные. + +Писать LIFT гораздо проще, чем его анализировать: экспортер генерирует только подмножество конструкций, которые создает его собственная модель, и никогда не сталкивается с опциональностью полной спецификации. Самое сложное — это детали: сопутствующий элемент `.lift-ranges`, текст для каждой системы письма, стабильные идентификаторы и экранирование XML — и именно их и выявляют приведенные ниже проверки. + +## Пакеты с застежкой-молнией + +LIFT обычно передаётся в виде одного файла `.zip` — программы FieldWorks и The Combine импортируют и экспортируют данные именно таким образом — поэтому `sil-lift` напрямую считывает и записывает заархивированные пакеты в любом из двух форматов, используемых в этой экосистеме: либо файлы находятся в корневом каталоге архива, либо вложены в одну папку верхнего уровня. + +- **Примечание:** Команда `sil_lift.load("package.zip")` распаковывает файл в временный каталог, находит единственный файл `.lift` и загружает его (сопутствующие файлы и мультимедиа обрабатываются как обычно). + - Команды CLI `validate`, `stats`, `check-media` и `export` также принимают путь к файлу `.zip`, поэтому приведенный ниже скрипт запускается для пакета в том виде, в котором он есть. + - `stats` и `export`, а также извлекать только файл `.lift`, а не весь пакет — таким образом, их обработка останется малозатратной даже при большом объеме медиаданных, а ограничение на извлечение будет применяться только к файлу `.lift`, а не ко всему остальному. + - Максимальный размер извлечения ограничен 10 ГиБ и 100 000 элементов; пакет, превышающий любой из этих пределов, отклоняется с ошибкой `LiftParseError`, равно как и тот, пути к элементам которого выходят за пределы каталога извлечения. +- **Напишите:** `Lexicon.save_zip("out.zip", wrap_folder="MyDict")` упаковывает файл `.lift`, его файлы `.lift-ranges`, а также все остальные файлы из исходной папки (медиафайлы, `WritingSystems/`, `consent/`, ...) в архив ZIP. + - По умолчанию `wrap_folder` — это папка верхнего уровня, названная в соответствии с именем ZIP-архива (согласно соглашению об импорте FieldWorks/Combine); для создания плоского архива передайте значение `False`. + +Файлы `.lift` и `.lift-ranges` сохраняют точность воспроизведения на уровне байтов внутри пакета; сам контейнер zip не обеспечивает точность воспроизведения на уровне байтов. + +## Проверить выходные данные в качестве контрольного критерия соответствия + +Укажите команде `sil-lift validate` путь к сгенерированному файлу `.lift`. Он выполняет проверку с помощью RELAX NG (как над `.lift`, так и над его сопутствующим элементом `.lift-ranges`), а также семантические проверки, которые не могут быть выражены с помощью грамматики: незавершенные ссылки на `relation`/`variant`, дубликаты GUID, целостность родительских элементов диапазона, значения черт и грамматической информации, не определённые в их диапазоне, а также ссылки в заголовке `range/@href`, которые не указывают на сопутствующий элемент. + +Для CI: в случае любой ошибки выдавать результаты в машиночитаемом формате: + +``` +sil-lift validate export.lift --strict --no-check-media --format json +``` + +- Параметр `--strict` приводит к сбою выполнения даже при появлении предупреждений (а не только ошибок). +- `--no-check-media` пропускает проверку наличия медиафайлов в файловой системе, поскольку результаты проверки `missing-media` являются ложными срабатываниями, если аудио- и фотофайлы в CI находятся не в той же папке, что и файл `.lift`. +- `--format json` выводит один объект JSON (`{"problems": [...], "summary": {...}}`) вместо текста, понятного человеку; его коды завершения и схема представляют собой поддерживаемый интерфейс, на который распространяется спецификация SemVer (см. [руководство по командной строке](cli.md)). +- `--require-ids` дополнительно выдает ошибку при обнаружении записей, в которых отсутствует `guid`, или при обнаружении элементов, в которых отсутствует `id` — это полезно, когда при последующем повторном импорте необходимо обновить данные, а не дублировать их. + +Защититесь от незаметной потери данных (типа сбоя, приводящего к потере информации при экспорте в плоский формат CSV), проверив статистику с помощью команды `stats --format json` для вашей исходной модели: + +``` +sil-lift stats export.lift --format json +``` + +В нём приводятся данные о количестве «записей», «значений», «примеров», «ссылок на мультимедиа», «языков», а также «характеристик» для каждого названия. + +### Запуск gate без инструментария Python + +В рамках непрерывной интеграции (CI) проекта на TypeScript или C# эту же проверку можно выполнить без установки Python с помощью встроенного GitHub Action: + +```yaml +- uses: sillsdev/python-sil-lift@v0.1.0 + with: + path: export.lift + strict: "true" + no-check-media: "true" + format: json +``` + +или образ контейнера, скомпилированный на основе файла `Dockerfile` из репозитория: + +``` +docker build -t sil-lift . +docker run --rm -v "$PWD:/work" -w /work sil-lift validate export.lift --strict +``` + +## Сопутствующий элемент `.lift-ranges` + +Контролируемые словари — части речи, семантические домены и любые другие наборы значений, организованные по признакам — хранятся в отдельном файле `.lift-ranges`, на который есть ссылка в файле `
`: + +```xml +
+ + + + +
+``` + +В справочнике приводится полное определение каждого диапазона. Значения представляют собой ``; `parent` формирует иерархию; `label` / `abbrev` / `description` являются мультитекстами: + +```xml + + + + + +
существительное
+
+
+ + + + + +
+``` + +Затем в записи происходит ссылка на значение по идентификатору: часть речи значения — ``, а семантическая область — ``. Команда `sil-lift validate` выдает предупреждение (`undefined-range-value`), если значение не определено в соответствующем диапазоне, и ошибку (`range-parent`), если `parent` не является идентификатором элемента одного уровня — поэтому указывайте диапазоны, которые фактически используются в ваших данных. См. также [Диапазоны и носители](folder-media.md). + +Если вы создаете экспорт на Python, функции `Lexicon.add_ranges_file()`, `RangesFile.add_range()` и `Range.add_element()` автоматически формируют сопутствующий объект и добавляют ссылки на заголовки; `open_writer(..., ranges=...)` выполняет то же самое для потокового пути. + +## Текст и несколько текстов + +Каждая строка на одном из человеческих языков в LIFT представляет собой _мультитекст_: по одному `
` для каждой системы письма, каждая из которых содержит ``: + +```xml + + kanga +
курица
+
+``` + +Модель, в которой строки индексируются по коду языка (тип `MultiString`, `Record`, `dict[str, str]`), отображается на эту модель в соотношении «один к одному»: каждая запись по ключу преобразуется в один объект `
`. В одном мультитексте допускается не более одной формы на каждый язык — в противном случае `sil-lift` выдает предупреждение `duplicate-form-lang`. + +Экранирование XML — это единственная часть, в которой действительно важно соблюдать точность. В тексте элемента символы `&`, `<`, and `>` должны быть экранированы (`&`, `<`, `>`); в значениях атрибутов также необходимо экранировать символ кавычки. Программа `sil-lift` строго следует этим правилам и никогда не изменяет пробелы внутри `` — она не добавляет там отступов, поскольку это привело бы к повреждению лексических данных. Если вы хотите добиться такого же результата, используйте экранирование, предусмотренное настоящим XML-сериализатором (а не самодельную замену, в которой упускается символ `&`), и оставьте содержимое `` без изменений, байт за байтом, так как оно есть в исходном коде. diff --git a/docs/ru/guides/read-edit-write.md b/docs/ru/guides/read-edit-write.md new file mode 100644 index 0000000..0a1af59 --- /dev/null +++ b/docs/ru/guides/read-edit-write.md @@ -0,0 +1,66 @@ +# Читать, редактировать, писать + +## Загрузка + +```python +import sil_lift + +lex = sil_lift.load("dictionary.lift") +``` + +Функция `load()` принимает любой корректно сформированный документ LIFT **0.13** — в том числе реальные файлы, не соответствующие схеме. Все, что не определено в модели (неизвестные элементы/атрибуты, комментарии), передается без потерь в виде остаточного данных LIFT в непрозрачном поле `extra` каждого узла. Другие версии LIFT вызывают исключение `LiftParseError`, указывая номер версии. + +## Модель + +Каждый элемент LIFT представляет собой типизированный класс данных: `Entry`, `Sense`, `Example`, `Pronunciation`, `Variant`, `Relation`, `Etymology`, `Reversal` и т. д. Многоязычный текст представляет собой объект `Multitext`, который работает как отображение кода языка на объект `Text`: + +```python +entry = lex.find(id="abat") + +str(entry.lexical_unit["seh"]) # "abat" +entry.lexical_unit["en"] = "grove" # простые строки преобразуются +"en" in entry.citation # False +``` + +`Text` имеет структуру — это упорядоченный список фрагментов `str` и `Span` — поскольку `` может содержать вложенную разметку ``. `str(text)` преобразует текст в обычный текст; фрагменты сохраняют разметку для обратного преобразования. + +В LIFT глоссы имеют форму _form_ (каждый `` содержит свой собственный язык), поэтому сенс имеет `glosses: list[Form]`, а также вспомогательную функцию: + +```python +sense = entry.senses[0] +sense.gloss("en") # Text | None +entry.gloss_langs() # {"en", "id"} +``` + +## Сохранение + +```python +lex.save() # сохранить в том же месте, откуда было загружено +lex.save("elsewhere.lift") +``` + +Записи, которые вы не изменяли, записываются обратно **байт за байтом**; документ, который вы не изменяли вообще, остается байт за байтом идентичным от первого до последнего байта. Точные условия договора см. в разделе [Гарантии Fidelity](../fidelity.md). + +## Создание с нуля + +```python +lex = sil_lift.Lexicon(producer="my-script 1.0") +entry = sil_lift.Entry(id="hello", guid="...") +entry.lexical_unit["en"] = "hello" +sense = sil_lift.Sense() +sense.glosses.append(sil_lift.Form("fr", sil_lift.Text(["bonjour"]))) +entry.senses.append(sense) +lex.entries.append(entry) +lex.save("new.lift") +``` + +## Каноническая сортировка + +```python +lex.sort() # записи по (guid, id); диапазоны/определения полей по id/тегу +lex.save() # неизменённые записи сохраняют свои точные байты в новом порядке + +sil_lift.canonicalize("in.lift", "out.lift") # полностью пересериализовано, готово к сравнению +``` + +См. также: [Пример с решением: массовое редактирование глосс](bulk-edit-glosses.md). diff --git a/docs/ru/guides/validate.md b/docs/ru/guides/validate.md new file mode 100644 index 0000000..e707f0f --- /dev/null +++ b/docs/ru/guides/validate.md @@ -0,0 +1,36 @@ +# Проверить + +Проверка всегда выполняется явно — при загрузке и сохранении проверка никогда не выполняется неявно. + +```python +import sil_lift + +# Исчерпывающий подход: ленивый поток объектов Problems (схема + семантические слои). +for problem in sil_lift.iter_problems("dictionary.lift"): + print(problem) + # ошибка [dangling-ref] dictionary.lift:88 (запись apu): ссылка 'nope' совпадает с ... + +# Быстрая остановка при ошибке: генерирует LiftValidationError при первой проблеме уровня ошибки. +sil_lift.validate_file("dictionary.lift") + +# Состояние в памяти (сначала сериализуется — задокументированная нагрузка при работе с большими лексиконами): +lex = sil_lift.load("dictionary.lift") +problems = list(lex.iter_problems()) +``` + +Каждая `Problem` содержит `level` (`"error"`/`"warning"`), постоянный `code`, `message` и адрес: `file`, `entry_id`, `guid`, `line`. + +## Слои + +1. **RELAX NG** в соответствии с грамматикой LIFT 0.13 (включенной из пакета lift-standard — это байт-идентичная копия, добавленная в данный пакет). +2. **Схема диапазонов** — файл `lift-ranges-0.13.rng` данного проекта — для каждого отслеживаемого компаньона `.lift-ranges`. +3. **Семантические проверки**, которые не могут быть выражены с помощью грамматики: `duplicate-guid`, `dangling-ref`, `range-parent`, `undefined-range-value`, `duplicate-form-lang`, `missing-media`. + +## Реальные результаты работы FieldWorks (FLEx) + +FieldWorks систематически генерирует контент, который отклоняется строгой системой проверки. Вот правила sil-lift, позволяющие обеспечить эффективную проверку подлинности лексиконов: + +- Ссылки `file://C:/...` (недопустимые URI) фиксируются как **предупреждения** (`uri-not-rfc`), а не как ошибки схемы — валидатор C# никогда их не отклонял. +- Дети, чередующиеся в соответствии с правилами (например, в некотором смысле `field, note, field, note`), **не** помечаются, что позволяет обойти ложное срабатывание в libxml2. +- Значения диапазонов сравниваются с использованием нормализации Unicode NFC — FLEx записывает файл `.lift` в формате NFC, а файл `.lift-ranges` — в формате NFD в рамках одного и того же экспорта. +- Расширения `trait`/`field` из FLEx внутри `range-element` **упоминаются** (ошибки схемы по отношению к схеме диапазонов): это настоящие отклонения от спецификации. diff --git a/docs/ru/index.md b/docs/ru/index.md new file mode 100644 index 0000000..4a0984b --- /dev/null +++ b/docs/ru/index.md @@ -0,0 +1,32 @@ +# sil-lift + +Библиотека на языке Python для [LIFT](https://github.com/sillsdev/lift-standard) (Lexicon Interchange FormaT) 0.13: чтение и запись папки LIFT без потерь (`.lift` + `.lift-ranges` + ссылки на медиафайлы), проверка схемы и семантики, а также каноническая сортировка — с потоковыми API для больших лексиконов. + +**Статус: предварительная версия, ведётся активная разработка.** + +## Установить + +С сайта [PyPI](https://pypi.org/project/sil-lift/): + +``` +pip install sil-lift # библиотека + команда sil-lift +``` + +Требуется Python версии 3.11 или выше. Единственной зависимостью на этапе выполнения является lxml. + +## 30-секундная экскурсия + +```python +import sil_lift + +lex = sil_lift.load("thesaurus.lift") # также отслеживает сопутствующие элементы с расширениями .lift + +for entry in lex.entries: + if "en" not in entry.gloss_langs(): + print(entry.id, str(entry.lexical_unit.get("seh") or "")) + +entry = lex.find(guid="0f5a9c3e-...") # или lex.find(id="hoofd_a1b2") +entry.senses[0].definition["en"] = "head (anatomy)" + +lex.save() # неизменённые записи остаются байтово идентичными; отредактированная запись сериализуется заново +``` diff --git a/docs/ru/reference.md b/docs/ru/reference.md new file mode 100644 index 0000000..e2fa4d0 --- /dev/null +++ b/docs/ru/reference.md @@ -0,0 +1,9 @@ +# Справочник по API + +Публичный API — это именно то, что `sil_lift` реэкспортирует на верхнем уровне; модули с префиксом «_» относятся к деталям реализации. + +::: sil_lift +options: +show_root_heading: false +show_source: false +members_order: source diff --git a/docs/sw/csharp-differences.md b/docs/sw/csharp-differences.md new file mode 100644 index 0000000..53a05c7 --- /dev/null +++ b/docs/sw/csharp-differences.md @@ -0,0 +1,52 @@ +# Tofauti na maktaba za C\# + +sil-lift ni mfano hafifu wa zana za LIFT za C# za SIL — hasa `SIL.Lift` katika [libpalaso](https://github.com/sillsdev/libpalaso) (mshinikizo, mhakiki, mhamishaji, `LiftSorter`), `SIL.DictionaryServices` katika repo hiyo hiyo (mfano wa `LexEntry`/`LexSense`, ukiwa na msomaji/mwandishi wake wa LIFT, ambao The Combine na WeSay hutumia), na vishughulikiaji vya LIFT katika [Chorus](https://github.com/sillsdev/chorus). Ni utekelezaji mpya, sio toleo lililohamishwa. Ukurasa huu unafupisha mahali tabia inatofautiana kwa makusudi. + +## Wigo + +| Uwezo | Maktaba za C# | sil-lift | +| ----------------------------------- | ---------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------- | +| Toleo za LIFT | 0.10–0.13 (uhamiaji umejengewa ndani) | **0.13 tu**; matoleo ya zamani yanakataliwa kwa kosa dhahiri | +| Uhamishaji wa toleo | `Migrator` (mnyororo wa XSLT) | Hakuna — tumia XSLTs katika lifti-kawaida kwa ajili ya masasisho ya mara moja | +| Muunganiko/Ulinganisho wa njia tatu | Kwaya | Nje ya wigo | +| Uthibitishaji | RELAX NG tu (`Validator`) | RELAX NG + masanduku ya schema + ukaguzi wa semantiki | +| Utiririshaji | Uchanganuzi wa ndani wa kiwango cha kuingia | API ya umma `open_reader` / `open_writer` | + +## Umbo la API + +Parser ya `SIL.Lift` inaendeshwa na callback (`ILexiconMerger`): inasukuma matukio ya uchanganuzi kwa mtumiaji. sil-lift badala yake hurudisha grafu ya vitu ya kawaida — dataclasses zilizotengwa aina kwa kila kipengele cha LIFT — kwa sababu watunzi wa skripti za Python wanataka vitu, si callbacks. `SIL.DictionaryServices` huweka juu ya `SIL.` mfano wa kitu wa `LexEntry`/`LexSense`Lift`, lakini kama mfano wa programu unawakilisha tu miundo inayotumiwa na programu hizo — hivyo kuirudisha tena katika muundo huu haiwezi kuhifadhi yaliyomo nje ya mfano kama vile usimamizi wa mabaki ya LIFT na uaminifu wa baiti wa sil-lift unavyofanya (tazama hapa chini). API ya utiririshaji hutoa aina ile ile ya `Entry\`, kwa hivyo hakuna modeli ya pili iliyopunguzwa ya kujifunza. + +## Uaminifu wa safari ya kwenda na kurudi + +Tofauti iliyokusudiwa yenye nguvu zaidi. Kuhifadhi kwa kutumia `SIL.Lift` kunaserialisha tena hati nzima. sil-lift inahakikisha: + +- Hati isiyobadilika huhifadhi **byte-identically**, na +- Maingizo yasiyoguswa huhifadhi baiti zao halisi za chanzo hata wakati maingizo mengine yanapobadilika — ni ukataji sawa wa baiti kwa kila ingizo unaotumiwa na Chorus, unaotumika kiotomatiki. + +Tazama [Dhamana za Fidelity](fidelity.md). + +## Uthibitishaji + +Validator ya C# hufanya upitaji mmoja wa RELAX NG na kuripoti makosa ya kwanza kama nyuzi. sil-lift inaripoti mtiririko uliopangiliwa wa `Problem`, kila mmoja ukiwa na faili, kipengee, na mstari unaohusika, na safu yake ya skema inakwenda kinyume kwa makusudi katika sehemu tatu: + +- **URI zisizo halali ni maonyo, si makosa.** Injini ya C# RELAX NG haijawahi kulazimisha aina ya data `anyURI`, hivyo FieldWorks (FLEx) imekuwa ikiandika hrefs za `file://C:/...` katika kamusi halisi kwa miaka mingi. Kukataa faili hizo kungeweka alama karibu kila toleo la FLEx. +- **Kanuni za Schematron zinatekelezwa** (kama ukaguzi wa kisemantiki): lugha za fomu zilizorudiwa na vikwazo vinavyofanana katika sarufi ya LIFT vilipuuzwa kimya kimya na C# na uthibitishaji wa lxml ghafi. +- Ulinganisho wa faili-msalaba ume-normalishwa kwa Unicode, kwa sababu FLEx huandika `.lift` katika NFC na faili mwenza `.lift-ranges` katika NFD. + +sil-lift pia huthibitisha faili za `.lift-ranges` zinazohusiana na kamusi iliyopakiwa dhidi ya skema ya nyaraka huru za masafa (zilizotolewa kutoka `lift-standard` pamoja na sarufi ya msingi ya LIFT) — kila faili ya masafa ya nje inayofuatiliwa hukaguliwa kila wakati `.lift` inapothibitishwa — bila mpangilio kama huo (au ukaguzi) katika ulimwengu wa C#. Hakuna njia ya kuthibitisha faili ya `.lift-ranges` peke yake, bila kuwa na faili ya `.lift`. + +## Upangaji rasmi + +`Lexicon.sort()` inaakisi kanuni za msingi za `LiftSorter` (vipengele kwa `guid` isiyozingatia herufi kubwa/ndogo; viwango na vipengele vya kiwango kwa `id`; ufafanuzi wa nyanja za kichwa kwa `tag`; maana zinahifadhiwa kulingana na mpangilio wa faili; nafasi tupu ndani ya `` haibadilishwi kamwe), na tofauti tatu: + +- Ingizo bila guid huorodheshwa kwa utaratibu wa uhakika kulingana na id (`LiftSorter` huchukulia kuwa guid ipo); +- Uagizaji hautegemei eneo (alama za kawaida zilizopinduliwa kwa herufi, si upangaji wa .NET wa utamaduni usiobadilika); +- Orodha za aina moja kama vile dondoo, mahusiano, na fomu zinadumisha mpangilio wao wa hati badala ya kupangwa upya kwa kutumia funguo — upangaji wa makundi tayari ni wa uhakika, na kuzipanga upya kunazidisha tu kelele za utofauti. + +`canonicalizeLift.xsl` ya repo ya spec haitumiki kabisa: inafuta nafasi tupu ndani ya maandishi ya kileksiki (huharibu) na vitambulisho vyake vinavyotengenezwa hutofautiana kila inapotekelezwa. + +## Haijahamishwa + +- Vifaa maalum vya WeSay (udhibiti wa dashibodi/usanidi kuhusu faili za LIFT). +- `SynchronicMerger` (Uunganishaji wa masasisho ya kwaya) — wazo la kugawanya baiti katika vipande linaendelea kuishi katika safu ya uaminifu, lakini uunganishaji wenyewe hauendelei. +- Uchanganuzi wa mfumo wa uandishi wa LDML: faili zilizo katika `WritingSystems/` zinachukuliwa kama yaliyomo yasiyoonekana ya folda. diff --git a/docs/sw/fidelity.md b/docs/sw/fidelity.md new file mode 100644 index 0000000..5393600 --- /dev/null +++ b/docs/sw/fidelity.md @@ -0,0 +1,33 @@ +# Dhamana za uaminifu + +LIFT ni muundo wa kubadilishana, kwa hivyo kanuni ya kwanza ni **kamwe usitupe kile usichokielewa**. Mkataba wa `sil-lift`, uliothibitishwa na mkusanyiko wa majaribio katika kila utekelezaji (faili za koropusi pamoja na uundaji unaotegemea sifa): + +## Kusoma + +Hati yoyote ya LIFT 0.13 iliyoundwa vizuri hupakiwa — ikijumuisha maudhui yasiyoendana na skema. Chochote ambacho mfano haujafafanua huhifadhiwa kwenye kontena la `Extras` lisilo wazi la nodi iliyo karibu zaidi kama _baki ya LIFT_ — jina la FieldWorks kwa wazo lile lile, ambalo huhifadhiwa katika uwanja wa `LiftResidue`: sifa na vipengele visivyojulikana, maoni ya XML na maagizo ya usindikaji, maandishi yaliyotapakaa, na sifa za aina zisizojengwa ipasavyo (tarehe isiyo sahihi hubaki kama msururu wa awali katika `Extras`; uwanja uliopangwa ni `None`). + +## Kuhifadhi hati bila mabadiliko + +`load()` → `save()` bila mabadiliko huandika **matokeo yanayofanana kabisa kwa baiti** — hakuna kupanga upya muundo, hakuna kurejesha tena alama za kutoroka, hakuna kupanga upya mpangilio, alama za mpangilio wa baiti na matangazo ya XML zimejumuishwa. Kwa sasa hakuna orodha ya kawaida: utambulisho ni sahihi kabisa. + +Vilevyo vya kipekee (mwandishi anarudi kwenye mfululizo kamili wa kanoniki, ambao kimaana ni kamili lakini hauhifadhi baiti): + +- Ukodishaji wa chanzo hauendani na ASCII (sio UTF-8/US-ASCII), au +- chanzo kina DOCTYPE, au +- Kichanganuzi cha baiti na kichanganaji havikubaliani kuhusu muundo wa ngazi ya juu wa hati — kwa mfano `
` ya pili isiyoendana na vigezo, ambayo kichanganaji huihifadhi mara moja tu (kichanganuzi cha baiti kinajihifadhi kwa tahadhari: kinapokuwa na shaka hakichukui baiti zozote za chanzo), au +- Chanzo kilijengwa kwenye kumbukumbu badala ya kupakiwa kutoka kwenye faili. + +## Kuhifadhi hati iliyohaririwa + +- **Maingizo yasiyoguswa hutolewa neno kwa neno kutoka kwa baiti zao za awali.** Ingizo linahesabiwa kuwa limeguswa ikiwa sehemu yoyote ya kitu chake cha mfano imebadilika tangu uchanganuzi (inagunduliwa na picha ya kanonali-serialization, si bendera ya uchafu). +- **Maingizo yaliyoguswa yanaserializwa tena kwa kanuni na kikamilifu**: UTF-8, uingizaji wa nafasi mbili _nje_ ya maudhui mchanganyiko (nafasi tupu ndani ya `` na `` haibadilishwi kamwe), upangaji wa vikundi vya watoto uliodokumentishwa kwa kila kipengele (mfano: ingizo: kitengo-cha-msamiati, nukuu, matamshi, aina-tofauti, maana, dondoo, uhusiano, asili-ya-neno, maelezo-ya-nyongeza, sifa, nyanja), mpangilio thabiti wa sifa, tarehe kwa ISO-8601 (`Z` kwa UTC). Baki yote inatolewa tena; nafasi yake inarejeshwa kwenye kiashiria cha awali cha mtoto, na kufungwa kwenye orodha mpya ya watoto (ni makadirio — nafasi halisi za baiti zinahakikishwa tu kwa vipengee visivyoguswa). +- Kuongeza, kuondoa, au kupanga upya maingizo kunasababisha muundo wa hati kusiriwa upya kwa mpangilio wa mfululizo, lakini bado hutoa baiti za kila kiingizo kisichobadilika neno kwa neno. + +!!! note """ rasmi hapa haihusiani na XML rasmi nyingine yoyote." + Fomu kanoniki kwenye ukurasa huu inamaanisha umbo halisi lililoandikwa la `sil-lift`, lililoelezwa katika nukta hapo juu. Haifungamani na mchakato wa W3C wa Canonical XML (C14N). Haijahusiana na darasa la `CanonicalXmlSettings` la `SIL.Core`. + +## Makadirio yanayojulikana (nodsi zilizoguswa pekee) + +- Maoni ndani ya utekelezaji wa `` huhifadhiwa lakini huhamishwa kando ya utekelezaji, badala ya kuwekwa mahali pake hasa kulingana na nafasi ya herufi. +- Agizo la mtoto la aina ya msalaba ndani ya kipengele kilichohaririwa linawekwa katika muundo wa kawaida wa makundi (shemia ya LIFT `interleave` hufanya mpangilio huu usiwe na maana kimaana). +- Kipengele cha multitext kilichopo lakini hakibebi chochote — hakuna fomu, hakuna mabaki, mfano `` — hakitolewa tena. Mfano unawakilisha nyanja hizi kama `Multitext` inayopatikana kila wakati (`lexical-unit`, `citation`, `definition`, `usage` ya uhusiano, na `label` / `abbrev` / `description` kwenye marejeleo ya URL, vipimo, vipengele vya kipimo na kichwa), hivyo tupu haiwezi kutofautishwa na kutokuwepo baada ya uchanganuzi. Hakuna kitu cha kisemantiki kinachopotea. diff --git a/docs/sw/guides/build-export.md b/docs/sw/guides/build-export.md new file mode 100644 index 0000000..45024b4 --- /dev/null +++ b/docs/sw/guides/build-export.md @@ -0,0 +1,168 @@ +# Mfano uliofanyiwa kazi: kujenga usafirishaji wa LIFT kutoka mwanzo + +Ikiwa unatuma data za programu nyingine kama LIFT — kazi iliyopo nyuma ya [Kutengeneza LIFT inayokidhi viwango](lift-export-interop.md) — `sil-lift` inaweza kujenga kipengee cha hati kipengee kwa kipengee na kukisierializa, badala ya kutoa XML kwa mkono. Hii inaelezea hatua kwa hatua skripti moja inayounda kipengee chenye vipengele ambavyo kamusi halisi ina (mifumo mingi ya uandishi, matamshi, maana yenye mfano, mfano wa picha, sifa ya eneo la semanti, na sehemu maalum kwa programu), inaandika kamusi zilizodhibitiwa katika faili mwandani la `.lift-ranges`, inathibitisha, na huhifadhi. + +## Maandishi + +```python +from pathlib import Path + +import sil_lift + +lex = sil_lift.Lexicon(producer="my-exporter") + +# Kuingia moja, iliyojengwa kutoka kwa mfano wa chanzo. +entry = sil_lift.Entry(id="kanga", guid="6b9e7c2a-3f4d-4a1b-8c5e-2d9f0a1b2c3d") +entry.lexical_unit["seh"] = "nkhuku" +entry.lexical_unit["pt"] = "galinha" + +pron = sil_lift.Pronunciation() +pron.forms["en"] = "Speaker: Ana" # Kanuni ya lebo ya mzungumzaji ya Combine +pron.media.append(sil_lift.URLRef(href="audio/nkhuku.wav")) +entry.pronunciations.append(pron) + +sense = sil_lift.Sense(id="kanga_s1") +sense.grammatical_info = sil_lift.GrammaticalInfo(value="Nomino") +sense.glosses.append(sil_lift.Form(lang="en", text=sil_lift.Text(["chicken"]))) +sense.definition["en"] = "a domestic fowl kept for its eggs and meat" + +example = sil_lift.Example() +example.forms["seh"] = "Ndinafuna nkhuku." +translation = sil_lift.Translation() +translation.forms["en"] = "Nataka kuku." +example.translations.append(translation) +sense.examples.append(example) + +photo = sil_lift.URLRef(href="pictures/hen.jpg") +photo.label["en"] = "Kuku" +sense.illustrations.append(photo) + +sense.traits.append(sil_lift.Trait(name="semantic-domain-ddp4", value="1.6.1.2")) + +scientific = sil_lift.Field(type="scientific-name") # uwanja wa ziada maalum kwa programu +scientific.content["en"] = "Gallus gallus domesticus" +sense.fields.append(scientific) + +entry.senses.append(sense) +lex.entries.append(entry) + +# Vichwa vya maneno vilivyodhibitiwa ambavyo kipengee kinavyorejelea, katika .lift-ranges. +ranges = sil_lift.RangesFile() +ranges.add_range("grammatical-info").add_element("Noun").label["en"] = "noun" +ranges.add_range("semantic-domain-ddp4").add_element("1.6.1.2").label["en"] = "Ndege" +lex.add_ranges_file(ranges, href="birds.lift-ranges") + +# Thibitisha kile save() itaandika, kabla ya kugusa diski. +problems = list(lex.iter_problems()) +print(f"uthibitishaji: {len(problems)} problem(s)") + +out = Path("export") +out.mkdir(exist_ok=True) +lex.save(out / "birds.lift") +print("=== birds.lift ===") +print((out / "birds.lift").read_text(encoding="utf-8"), end="") +print("=== birds.lift-ranges ===") +print((out / "birds.lift-ranges").read_text(encoding="utf-8"), end="") +``` + +## Kinachozalisha + +`uthibitishaji: 0 matatizo`, kisha `.lift` na mwenzake kando kwa kando: + +``` +=== birds.lift === + + +
+ + + + +
+ + + + nkhuku + +
+ galinha +
+
+ +
+ Speaker: Ana +
+ +
+ + + + chicken + + +
+ a domestic fowl kept for its eggs and meat +
+
+ +
+ Ndinafuna nkhuku. +
+ +
+ I want a chicken. +
+
+
+ + + + + +
+ Gallus gallus domesticus +
+
+
+
+
+=== birds.lift-ranges === + + + + + + + + + + + + + +``` + +## Maelezo kuhusu API + +- Maeneo ya maandishi mengi (`lexical_unit`, `definition`, lebo ya `Form`/`URLRef`, yaliyomo ya `Field`, ...) Chukua mnyororo mmoja kwa kila mfumo wa uandishi kupitia kiolesura cha upangaji: `entry.lexical_unit["seh"] = "nkhuku"` huongeza `
`. Mfano wa chanzo unaotumia misimbo ya lugha kupanga nyuzi unaendana moja kwa moja na hili. +- `RangesFile.add_range()` / `Range.add_element()` hujenga vokabularia zilizodhibitiwa, na `Lexicon.add_ranges_file(ranges, href=...)` huunganisha faili ya ziada na kuongeza kichwa `` kwa marejeleo — hivyo `` na `` za kipengee hurejelea masafa uliyoyabainisha. +- URLRef ni href pamoja na maandishi mengi ya kichwa/lebo ya hiari — hutumika kwa `` (sauti) na `` (picha). Matamshi hapa yanafuata kanuni ya The Combine ya aina ya 'en', kama inavyosomwa na msemaji: +- Data maalum kwa programu bila safari za nyumbani za LIFT za asili kama `` (au ``): FieldWorks husoma hizi kama viwanja maalum na The Combine huzihifadhi. +- Panga kila kipengee na `guid` halisi, thabiti (kwa mfano kutoka `uuid.uuid4()`), inayotumika tena katika usafirishaji wa data — kuingiza tena baadaye husasisha kipengee mahali pake badala ya kuunda nakala yake. `sil-lift validate --require-ids` inahakikisha hili. +- `lex.iter_problems()` inathibitisha hati iliyopo kwenye kumbukumbu (kile ambacho `save()` ingeandika) kabla chochote haijawekwa kwenye diski; hapa iko safi. Kwa sababu kamusi bado haina folda, ukaguzi wa media-presence na companion-href umeachwa — endesha [`sil-lift validate`](cli.md) kwenye matokeo yaliyohifadhiwa (au kwa kutumia `--no-check-media`) mara tu faili za sauti na picha zitakapokuwa zimewekwa. + +## Ufungashaji + +`lex.save("export/birds.lift")` huandika muundo wa saraka (`.lift` + `.lift-ranges` kando kando). Ili kutoa kifurushi kimoja cha zip ambacho FieldWorks na The Combine huingiza moja kwa moja, tumia `lex.save_zip("birds.zip")` badala yake — angalia [Kutengeneza LIFT inayofuata viwango](lift-export-interop.md). diff --git a/docs/sw/guides/bulk-edit-glosses.md b/docs/sw/guides/bulk-edit-glosses.md new file mode 100644 index 0000000..fbca531 --- /dev/null +++ b/docs/sw/guides/bulk-edit-glosses.md @@ -0,0 +1,79 @@ +# Mfano uliofanyiwa kazi: kuhariri kwa wingi tafsiri fupi + +Kazi ya kawaida ya matengenezo: kurekebisha tahajia ili iwe sawa katika kila fasili ya Kiingereza kwenye kamusi (kutoka Kiingereza cha Uingereza hadi Kiingereza cha Marekani, au kinyume chake) bila kuathiri chochote kingine katika faili. Hii inaelezea hatua kwa hatua skripti moja inayopakia, kuhariri, kuthibitisha, na kuhifadhi — ikionyesha API ya uhariri na dhamana ya uaminifu zikifanya kazi pamoja. + +## Maandishi + +```python +import sys + +import sil_lift + +path = "dictionary.lift" +lex = sil_lift.load(path) + + +def iter_senses(senses): + """Yeyeza kila hisia, ikiwa ni pamoja na hisia ndogo (kwa kujirudia).""" + for sense in senses: + yield sense + yield from iter_senses(sense.subsenses) + + +edited_glosses = 0 + +for entry in lex.entries: + for sense in iter_senses(entry.senses): + for gloss in sense.glosses: + if gloss.lang != "en": + continue + old = str(gloss.text) + new = old.replace("colour", "color") + if new != old: + gloss.text = sil_lift.Text([new]) + edited_glosses += 1 + +changed = lex.changed_entries() + +errors = [p for p in lex.iter_problems() if p.level == "error"] +if errors: + for problem in errors: + print(problem) + sys.exit(f"aborting: {len(errors)} validation error(s), nothing saved") + +lex.save() +print(f"edited {edited_glosses} gloss(es) across {len(changed)} entry(ies)") +``` + +Mambo machache ya kuzingatia: + +- `Sense.subsenses` ni `list[Sense]` yenyewe, kwa hivyo `iter_senses` inarudia ndani yake — uhariri wa jumla ambao ungepitia tu `entry.senses` ungeacha kimya kimya fasili yoyote iliyoko chini ya subsense. +- `gloss.text` ni `Text`, si mfululizo wa kawaida: `str(gloss.text)` huifanya iwe mfululizo wa kawaida kwa ajili ya kulinganisha, na mbadala huandikwa tena kwa kutumia `sil_lift.Text([new])` badala ya kubadilisha mfululizo mahali pake. +- `lex.changed_entries()` huripoti ni vipengee gani vinatofautiana na faili kama ilivyoandaliwa. Kwa kuwa muhtasari wa kijumla wa kipengee unajumuisha tawi lake lote, uhariri wa kipengele kidogo kilichojumuishwa huripoti kipengee kinachokijumuisha. + - Inalinganisha maudhui yaliyopangwa kwa mfululizo, hivyo kutoa uwanja thamani yake ya awali hakuripotiwi. + - Inaripoti mabadiliko ya maudhui pekee; `lex.added_entries()` na `lex.removed_entries()` zinashughulikia maingizo yaliyoonekana au yaliyotoweka tangu kupakia. + - Inarudisha maingizo yenyewe, bila kuathiriwa na `id` kurudiwa au kutokuwepo (jambo ambalo LIFT inaruhusu). + - Kama hesabu, ina maana tu pale panapokuwa na kitu cha kulinganisha nacho. Wakati skana ya baiti inapokataa kusoma chanzo — kodishaji ambao hauendani na ASCII, au kutokubaliana kwa skana na mchanganuzi — hakuna msingi, na `changed_entries()` inaripoti kila kitu. Hiyo ndiyo jibu la kweli kwa kizuiaji cha kuandika, kwa kuwa `save()` inasababisha kusajili tena faili nzima katika kesi hiyo, lakini hiyo inamaanisha kuwa hesabu ni ukubwa wa kamusi badala ya ukubwa wa uhariri. +- `lex.changes()` huripoti kama hati imebadilika _kabisa_. Haijumuishi tu maingizo, bali pia kichwa, kipengele cha mizizi, na kila mwandani wa `.lift-ranges`. + - Ni uongo tu wakati `save()` ingezaa tena baiti za chanzo, jambo linalofanya `if not lex.changes(): ...` kuwa njia sahihi ya kuepuka uandishi usiohitajika. Dhamana inaendeshwa upande mmoja: hairipoti kamwe "hakuna cha kuandika" kwa hati ambayo ingeandikwa upya, wakati mabadiliko yanayolazimisha urekebishaji kamili wa mfululizo yanaweza kurudi kwenye baiti za awali na bado kuripotiwa. + - Inalinganisha yaliyomo, si mahali pa mwisho, kwa hivyo linda tu uhifadhi mahali pake: `lex.save(some_other_dir / "dictionary.lift")` huandika hati na viambatisho vyake kwenye eneo ambalo bado halina chochote, bila kujali kama kitu kimebadilika au la. + - Ni kizuizi, si kiongezi kasi — kujibu hufanyia uchambuzi kila kipengee, kazi ile ile ambayo `save()` hufanya ili kuamua ni baiti gani za chanzo inaweza kutumia tena, hivyo unachokikwepa ni uandishi wenyewe (muda wa mabadiliko ya faili haujabadilika, hakuna tofauti isiyo ya lazima), si jitihada za kuamua. +- Uhakiki katika kumbukumbu (`lex.iter_problems()`) huweka kwanza hali iliyohaririwa kwa mpangilio, hivyo inaonyesha kwa usahihi mabadiliko kabla ya chochote kuandikwa kwenye diski. Kukata shughuli kwa `Problem` yoyote ya kiwango cha `"error"` — maonyo huachwa kwa mtu anayeita ili aamue — kunamaanisha kuwa uhariri mbaya hautawahi kufikia `save()`. + +Si glosi pekee ndizo zinazostahili kuguswa kwa njia hii. Uso uleule wa ramani wa `Multitext` unatumika kwa ufafanuzi na kila uwanja mwingine wa lugha nyingi kwenye kipengee au maana: + +```python +maana.fasili["en"] = "rangi ya kitu" +``` + +## Kuendesha + +Fanya utafutaji dhidi ya kamusi ndogo yenye fasili na fasili ya aina ndogo, zote zikisema "rangi": + +``` +Imehaririwa: tafsiri 2 katika kipengee 1 +``` + +## Faida ya uaminifu + +Dhamana ni kwa kila _entry_: entry ambayo mfano wake haukubadilika hurudi **byte-identical** kama ilivyosomwa, na ni tu entries ulizogusa ndizo zinazotengenezwa tena. Katika mfululizo hapo juu, kipengee kimoja kilikuwa na tafsiri zilizohaririwa — kila kipengee kingine katika faili kilidumisha baiti zake hasa. (Kumbuka kiwango cha undani: kuhariri sehemu yoyote ya kumbukumbu kunasababisha kumbukumbu yote kusimbwa upya kwa mpangilio, ikiwemo hisia zake ndugu ambazo hazijaguswa.) Kurekebisha fasili moja katika kamusi yenye maingizo 50,000 kwa hivyo hutoa tofauti inayogusa kiingizo kimoja, si faili iliyopangwa upya. Tazama [Dhamana za Fidelity](../fidelity.md) kwa mkataba sahihi. diff --git a/docs/sw/guides/cli.md b/docs/sw/guides/cli.md new file mode 100644 index 0000000..ac55ded --- /dev/null +++ b/docs/sw/guides/cli.md @@ -0,0 +1,69 @@ +# Mstari wa amri + +Kusakinisha kifurushi (`pip install sil-lift`) pia husakinisha amri ya `sil-lift` — zana inayoungwa mkono katika mtindo wa LiftTools inayokuja na kifurushi hicho (na, kwa `validate`, mfano uliofanyiwa kazi wa API ya maktaba). + +``` +sil-lift validate PATH [--format {text,json}] [--strict] [--no-check-media] [--require-ids] + matatizo yote, na faili/kipengee/mstari; kutoka 1 kwa makosa +sil-lift stats PATH [--format {text,json}] + idadi za kipengee/maana/lugha (mtiririko; ukubwa wowote) +sil-lift sort PATH [-o OUT] imepangwa kihalali, nakala tayari kwa tofauti (chaguo-msingi: mahali pake) +sil-lift check-media NJIA ripoti ya vyombo vilivyokosekana na vilivyoachwa peke yake; toa 1 ikiwa hakuna +sil-lift export NJIA [-o OUT] [--langs L] [--tsv] + safu moja kwa kila hisia ya majani (hisia ndogo zimepandishwa) kwa CSV/TSV (mtiririko) +``` + +`--format json` huandika object moja ya JSON kwenye stdout (na hakuna kitu kingine) kwa matumizi ya CI/otomatishaji; tazama schema katika mfano hapa chini. `--strict` huchukulia maonyo kama makosa, na kutoka kwa nambari 1 endapo yoyote yatapatikana — itumie ili kuidhinisha ujenzi endapo hakuna maonyo kabisa, badala ya kutegemea makosa pekee. `--no-check-media` hupuuza ukaguzi wa uwepo wa media kwenye mfumo wa faili (na hivyo kuficha matokeo ya `missing-media`), jambo ambalo ni muhimu wakati wa kuthibitisha toleo jipya lililotengenezwa ambalo faili zake za sauti/picha ziko mahali pengine badala ya kwenye folda moja. `--require-ids` pia hushindwa (kosa la `missing-id`) kwa kipengee chochote kinachokosa `guid` au sense kinachokosa `id` — ni kali zaidi kuliko LIFT, kwa mtiririko wa kazi unao-re-import tena kwa kutumia id thabiti. Kupitisha `-` kama njia husoma hati kutoka stdin (hati iliyopitishwa kwa bomba haina folda, hivyo faili zake za `.lift-ranges` na media hazitatatuliwa). `stats` vivyo hivyo huchukua `--format json`, na kutoa hesabu kama kitu kimoja cha JSON. + +!!! note + Misimbo ya kutoka ya `validate` na `--format json` schema ni kiolesura cha kiotomatiki kinachotumika: vyote vimejumuishwa katika majaribio na hubadilika tu kulingana na SemVer. + +`sort` inaandika upya tu faili ya `.lift`; faili zake washirika za `.lift-ranges` zinaachwa bila kuguswa (ziandike kwa mpangilio tofauti kwa kutumia API ya `RangesFile`). + +`validate`, `stats`, `check-media`, na `export` pia hukubali kifurushi cha LIFT kilichobanwa (faili la `.zip` katika mpangilio wowote — faili zikiwa kwenye msingi wa hifadhi, au zimewekwa ndani ya folda moja ya ngazi ya juu); hutolewa kwenye saraka ya muda na kutupwa wakati amri inapomalizika. Amri za streaming `stats` na `export` huchukua tu `.lift` yenyewe, hivyo zinabaki nafuu kwa vifurushi vyenye media nyingi; `validate` na `check-media` zinahitaji folda nzima na huchukua yote. + +Mifano: + +``` +$ sil-lift validate dictionary.lift +kosa [dangling-ref] dictionary.lift:88 (kipengee apu): ref 'nope' haifanani na kitambulisho chochote cha kipengee au kitambulisho cha maana +onyo [uri-not-rfc] dictionary.lift:6: : Herufi ya diski ya Windows inatumiwa kama mamlaka ya URI (mtindo wa FLEx file://C:/) +1 kosa, 1 onyo + +$ sil-lift validate dictionary.lift --format json +{ + "problems": [ + { + "level": "error", + "code": "dangling-ref", + "message": "ref 'nope' inalingana na id/guid ya entry au sense id yoyote", + "file": "dictionary.lift", + "entry_id": "apu", + "guid": null, + "line": 88 + }, + { + "level": "warning", + "code": "uri-not-rfc", + "message": ": Herufi ya diski ya Windows inatumiwa kama mamlaka ya URI (mtindo wa FLEx file://C:/)", + "file": "dictionary.lift", + "entry_id": null, + "guid": null, + "line": 6 + } + ], + "summary": { + "errors": 1, + "warnings": 1 + } +} + +$ sil-lift stats sango.lift +entries: 3507 +senses: 4541 +... + +$ sil-lift export dictionary.lift --langs en,fr -o dictionary.csv +``` + +Misimbo ya kutoka: `0` mafanikio (maonyo yanaruhusiwa, isipokuwa `--strict`), `1` matokeo (makosa ya uthibitishaji / vyombo vya habari vilivyokosekana / maonyo chini ya `--strict`), `2` ingizo lisilosomeka. diff --git a/docs/sw/guides/folder-media.md b/docs/sw/guides/folder-media.md new file mode 100644 index 0000000..61561f2 --- /dev/null +++ b/docs/sw/guides/folder-media.md @@ -0,0 +1,41 @@ +# Folda ya LIFT: masafa na vyombo vya habari + +Kamusi ya LIFT kawaida ni _folda_: faili ya `.lift`, moja au zaidi ya faili wasaidizi za `.lift-ranges` (faili za pembeni), na midia ya `audio/` / `pictures/`. + +## Vipimo + +```python +lex = sil_lift.load("dictionary.lift") # wenzao hufuatiliwa kiotomatiki + +lex.ranges_files # {Path(...): RangesFile} +lex.all_ranges() # mtazamo uliounganishwa wa {id: Range} +lex.all_ranges()["grammatical-info"].elements +``` + +Ugunduzi wa Companion unashughulikia ulimwengu halisi: `range/@href` inayoelekeza kwenye faili iliyopo hutumika; Viungo kamili vilivyokatika vya FieldWorks (`file://C:/...`) hurudi kwenye jina la msingi la faili lililo kwenye `.lift`; na ndugu wa kawaida `.lift-ranges` huchukuliwa hata kama hakuna kinachorejelea. + +`lex.save()` huandika `.lift` na kila mwandani aliyefuatiliwa pamoja. Marekebisho kwenye `RangesFile` huhifadhiwa tena kwenye faili yake; vipimo visivyoguswa hubaki na baiti zao halisi. Matumizi ya peke yake: + +```python +ranges = sil_lift.RangesFile.load("dictionary.lift-ranges") +ranges.find("grammatical-info") +ranges.sort() +ranges.save() +``` + +Pitisha `resolve_ranges=False` kwenye `load()` ili kupuuza ugunduzi wa vifaa viendani. + +## Vyombo vya habari + +```python +kwa ref katika lex.media_refs(): # kila na + print(ref.kind, ref.href, ref.entry_id) + +lex.missing_media() # marejeleo ambayo faili zao hazipo +``` + +Resolution inafuata mpangilio wa kawaida: href ya jamaa inachunguzwa kama ilivyo (backslashes zimewekwa sawa — WeSay inaandika `pictures\photo with space.png`) na chini ya `audio/` (kwa vyombo vya matamshi) au `pictures/` (kwa michoro). Href za mbali/kamili haziwezi kukaguliwa na hupitishwa. + +## Maudhui mengine ya folda + +Folda ya LIFT mara nyingi huwa na faili ambazo sil-lift haizifanyi modeli — LDML ya mfumo wa uandishi chini ya `WritingSystems/`, faili za sauti/picha za idhini za The Combine chini ya `consent/`, na kadhalika; `load()`/`save()` hazibadilishi hizi, na [`Lexicon.save_zip()`](lift-export-interop.md) huzihamisha neno kwa neno wakati wa kufunga folda. diff --git a/docs/sw/guides/large-files.md b/docs/sw/guides/large-files.md new file mode 100644 index 0000000..c4e472f --- /dev/null +++ b/docs/sw/guides/large-files.md @@ -0,0 +1,27 @@ +# Faili kubwa (mtiririko) + +`load()` huunda grafu nzima ya vitu. Kwa kamusi zenye mamia ya MB, API ya mtiririko huchakata kipengee kimoja kwa wakati katika kumbukumbu yenye ukomo — aina ile ile ya `Entry`, hivyo msimbo ulioandikwa kwa njia moja unafanya kazi katika njia nyingine. + +```python +import sil_lift + +with sil_lift.open_reader("big.lift") as reader: + header = reader.header # imeparswa mapema (kabla ya entries) + for entry in reader: # lazy Iterator[Entry] + ... +``` + +```python +with sil_lift.open_reader("big.lift") as reader, sil_lift.open_writer( + "out.lift", header=reader.header, producer="my-script" +) as writer: + for entry in reader: + if not entry.date_deleted: # mfano, ondoa tombstones + writer.write(entry) +``` + +Maelezo: + +- Matokeo ya mwandishi ni sawa kabisa na kile ambacho serializer kanoniki kamili ya hati ingetengeneza kwa maudhui yale yale — modi hizo mbili haziwahi kutofautiana. +- Modi ya utiririshaji haitumii tena baiti zozote za chanzo: pato daima ni sahihi. Baki ya LIFT ya kiwango cha mizizi — maoni kati ya vipengele na sifa zisizo za mpangilio kwenye `` — haibebwi; vipengele na kichwa ni kamili, ikijumuisha baki. +- Ikiwa bloku ya `open_writer` itapandishwa, faili inabaki haijakamilika wazi (bila `` ya kufunga) — kamusi iliyoandikwa nusu haipaswi kuonekana imekamilika. diff --git a/docs/sw/guides/lift-export-interop.md b/docs/sw/guides/lift-export-interop.md new file mode 100644 index 0000000..e55619c --- /dev/null +++ b/docs/sw/guides/lift-export-interop.md @@ -0,0 +1,112 @@ +# Kutengeneza LIFT inayokidhi viwango + +Mwongozo huu ni kwa yeyote anayeandika _exporter_ ya LIFT — msimbo katika lugha yoyote unaobadilisha muundo wa data wa programu nyingine kuwa LIFT 0.13. `sil-lift` ina majukumu mawili katika kazi hiyo: lango la utimilifu linalokagua pato dhidi ya skema na semantiki ambazo skema haiwezi kueleza, na rejea ya maumbo na kanuni za maandishi ambazo pato linapaswa kufuata. + +Kuandika LIFT ni rahisi zaidi kuliko kuchanganua: kiendeshaji cha kusafirisha hutoa tu sehemu ndogo ya miundo ambayo mfano wake wenyewe huzalisha, na kamwe hakikabiliwi na chaguzi zote za kiwango kamili. Sehemu ngumu ni maelezo — `.lift-ranges` companion, maandishi kulingana na mfumo wa uandishi, vitambulisho thabiti, na kuepuka XML — na hayo ndiyo hasa yanayokamatwa na ukaguzi hapa chini. + +## Vifurushi vilivyosimbwa + +LIFT kawaida huhamishwa kama faili moja ya `.zip` — FieldWorks na The Combine zote huingiza na kusafirisha kwa njia hiyo — hivyo `sil-lift` husoma na kuandika vifurushi vilivyofungwa kwa `zip` moja kwa moja, katika mpangilio wowote ambao mfumo unatumia: faili ziko mizizi ya hifadhi, au zimepangwa ndani ya folda moja kuu. + +- Soma: `sil_lift.load("package.zip")` hutoa maudhui kwenye saraka ya muda, hupata faili moja la `.lift`, na kulipakia (viendani na vyombo vya habari hutatuliwa kama kawaida). + - Amri za CLI `validate`, `stats`, `check-media`, na `export` pia zinakubali njia ya `.zip`, hivyo lango hapa chini linafanya kazi dhidi ya kifurushi kama kilivyo. + - mtiririko wa `stats` na `export`, na uchimbe tu `.lift` badala ya kifurushi kizima — ili viwe nafuu kwenye mfumo wenye media nyingi, na kikomo cha uchimbaji kitumike kwa `.lift` pekee badala ya kila kitu kingine. + - Utoaji umepunguzwa hadi 10 GiB na wanachama 100,000; kifurushi kinachovuka mojawapo ya mipaka hiyo kinakataliwa kwa `LiftParseError`, vivyo hivyo kifurushi ambacho njia za wanachama zake zinatoka nje ya saraka ya uondoaji. +- **Andika:** `Lexicon.save_zip("out.zip", wrap_folder="MyDict")` hufunga `.lift`, `.lift-ranges` zake, na kila faili nyingine katika folda ya chanzo (media, `WritingSystems/`, `consent/`, ...) katika zipu + - `wrap_folder` kwa chaguo-msingi huunda folda ya ngazi ya juu inayopewa jina la faili la zip (utaratibu wa kuingiza wa FieldWorks/Combine); toa `False` ili kupata hifadhi tambarare. + +`.lift` na `.lift-ranges` huhifadhi uaminifu wa baiti ndani ya kifurushi; chombo cha zip chenyewe hakiruhusu kurejesha baiti kikamilifu. + +## Thibitisha pato kama lango la utii + +Lenga `sil-lift validate` kwenye faili ya `.lift` iliyotengenezwa. Inatekeleza RELAX NG (kupitia `.lift` na mwenza wake `.lift-ranges`) pamoja na ukaguzi wa kisemantiki ambao sarufi haiwezi kueleza: marejeleo ya `relation`/`variant` yasiyo na kiungo, GUID zilizorudiwa, uadilifu wa wazazi wa vipengele vya safu, thamani za sifa na taarifa za kisarufi ambazo hazijaelezwa katika safu zao, na marejeleo ya `range/@href` katika kichwa yanayotatua hadi kwa mwandani asiye na muunganisho. + +Kwa CI, shindwa katika chochote na kutoa matokeo yanayosomeka na mashine: + +``` +sil-lift validate export.lift --strict --no-check-media --format json +``` + +- `--strict` hufanya maonyo (sio tu makosa) kusababisha utekelezaji kushindikana. +- `--no-check-media` hupuuza ukaguzi wa uwepo wa media kwenye mfumo wa faili, ambao matokeo yake ya `missing-media` ni kelele tu wakati faili za sauti/picha haziko kwenye folda moja na `.lift` katika CI. +- `--format json` huchapisha kitu kimoja cha JSON (`{"problems": [...], "summary": {...}}`) badala ya maandishi ya kawaida; misimbo yake ya kutoka na schema ni kiolesura kinachotumika kinachofunikwa na SemVer (tazama [mwongozo wa mstari wa amri](cli.md)). +- `--require-ids` pia hutoa hitilafu kwa maingizo yanayokosa `guid` au `id` — ni muhimu wakati uingizaji upya wa baadaye unapaswa kusasisha badala ya kurudia. + +Jikinga dhidi ya upotevu wa data kimya (mtindo wa kushindwa unaofanya usafirishaji wa CSV wa kawaida upoteze data) kwa kuthibitisha idadi kwa kutumia `stats --format json` dhidi ya mfano wako wa chanzo: + +``` +sil-lift takwimu toa.lift --format json +``` + +Inaripoti idadi ya `entries`, `senses`, `examples`, `media_refs`, `languages`, na `traits` kwa kila jina. + +### Kuendesha lango bila mnyororo wa zana za Python + +CI ya mradi wa TypeScript au C# inaweza kufanya ukaguzi uleule bila kusakinisha Python, kupitia GitHub Action iliyojumuishwa: + +```yaml +- matumizi: sillsdev/python-sil-lift@v0.1.0 + na: + njia: export.lift + strict: "kweli" + no-check-media: "kweli" + format: json +``` + +au picha ya kontena, iliyojengwa kutoka kwa `Dockerfile` ya repo: + +``` +docker build -t sil-lift . +docker run --rm -v "$PWD:/work" -w /work sil-lift validate export.lift --strict +``` + +## Mwandani wa `.lift-ranges` + +Vibainishi vilivyodhibitiwa — aina za maneno, nyanja za maana, na seti nyingine yoyote ya thamani zilizo na ufunguo wa sifa — huishi katika faili ndugu ya `.lift-ranges`, inayorejelewa kutoka kwenye `
`: + +```xml +
+ + + + +
+``` + +Kiambatisho kinabeba ufafanuzi kamili wa kila safu. Thamani ni ``; `parent` huunda ngazi; `label` / `abbrev` / `description` ni maandishi mengi: + +```xml + + + + + +
n
+
+
+ + + + + +
+``` + +Kisha, kila kipengee kinarejelea thamani kwa kutumia ID: sehemu ya hotuba ya hisia ni ``, na uwanja wa semantiki ni ``. `sil-lift validate` inatoa onyo (`undefined-range-value`) wakati thamani haijafafanuliwa katika upeo wake na makosa (`range-parent`) wakati `parent` si id ya ndugu — kwa hivyo toa upeo ambao data yako inatumia kweli. Tazama pia [Vipimo na vyombo vya habari](folder-media.md). + +Ukijenga toleo la kusafirisha kwa kutumia Python, `Lexicon.add_ranges_file()`, `RangesFile.add_range()`, na `Range.add_element()` huunda kiambatisho na kuongeza marejeleo ya kichwa kwa niaba yako; `open_writer(..., ranges=...)` hufanya vivyo hivyo kwenye njia ya utiririshaji. + +## Maandishi na maandishi mengi + +Kila mfululizo wa lugha ya kibinadamu katika LIFT ni _multitext_: `
` moja kwa kila mfumo wa uandishi, kila moja ikizunguka ``: + +```xml + + kanga +
kuku
+
+``` + +Mfano unaopanga nyuzi kwa msimbo wa lugha (MultiString, Record, dict[str, str]) unaakisi uwiano wa moja kwa moja: kila kipengee kwa kila ufunguo kinakuwa
moja. Kila lugha inaweza kuwa na fomu moja tu katika maandishi mengi — vinginevyo `sil-lift` itatoa onyo la `duplicate-form-lang`. + +Utoaji wa XML ni sehemu pekee inayohitaji usahihi hasa. Katika maandishi ya elementi, `&`, `<`, and `>` lazima ziwekwe kwa alama za kutoroka (`&`, `<`, `>`); katika thamani za sifa, pia alama ya nukuu. Mwandishi wa `sil-lift` hutumia kanuni hizi hasa na kamwe haibadilishi nafasi tupu ndani ya `` — haiongezi nafasi ya kuanzishia hapo, kwa sababu hilo lingeharibu data ya kisemaji. Ikiwa unalenga kuendana na matokeo yake, tumia tena mbinu halisi ya serializer ya XML ya kuepuka alama (sio uingizaji uliofanywa kwa mkono unaosahau `&`) na acha maudhui ya `` byte kwa byte kama chanzo chako kilivyo. diff --git a/docs/sw/guides/read-edit-write.md b/docs/sw/guides/read-edit-write.md new file mode 100644 index 0000000..cbb41ff --- /dev/null +++ b/docs/sw/guides/read-edit-write.md @@ -0,0 +1,66 @@ +# Soma, hariri, andika + +## Inapakia + +```python +import sil_lift + +lex = sil_lift.load("dictionary.lift") +``` + +`load()` hukubali hati yoyote ya LIFT **0.13** iliyopangwa vizuri — ikiwa ni pamoja na faili halisi zisizoendana na skema. Chochote ambacho mfano haujafafanua (vipengele/sifa zisizojulikana, maoni) huhifadhiwa bila kupoteza kama mabaki ya LIFT katika uwanja wa `extra` usio wazi wa kila node. Toleo zingine za LIFT hutoa `LiftParseError` ikitaja toleo. + +## Mfano + +Kila kipengele cha LIFT ni dataclass iliyotengwa aina: `Entry`, `Sense`, `Example`, `Pronunciation`, `Variant`, `Relation`, `Etymology`, `Reversal`, na kadhalika. Maandishi ya lugha nyingi ni `Multitext`, ambayo hufanya kazi kama uhusiano kutoka kwa msimbo wa lugha hadi `Text`: + +```python +entry = lex.find(id="abat") + +str(entry.lexical_unit["seh"]) # "abat" +entry.lexical_unit["en"] = "grove" # nyuzi rahisi zinageuzwa +"en" in entry.citation # Si kweli +``` + +`Text` imepangwa — orodha iliyopangwa ya vipande vya `str` na `Span` — kwa sababu `` inaweza kuwa na markup ya `` iliyojificha ndani yake. `str(text)` inageuza kuwa maandishi ya kawaida; vipande vinabaki na alama za uandishi ili kuruhusu kurudi nyuma. + +Glosses ni _form-shaped_ katika LIFT (kila `` lina lugha yake mwenyewe), hivyo sense ina `glosses: list[Form]` pamoja na kisaidizi: + +```python +sense = entry.senses[0] +sense.gloss("en") # Text | None +entry.gloss_langs() # {"en", "id"} +``` + +## Akiba + +```python +lex.save() # kurudi mahali ilipopakiwa kutoka +lex.save("elsewhere.lift") +``` + +Maingizo ambayo hukuyabadilisha yamerudishwa **sawa kwa baiti**; hati ambayo hukuyabadilisha kabisa ni sawa kwa baiti kuanzia baiti ya kwanza hadi ya mwisho. Tazama [Dhamana za Fidelity](../fidelity.md) kwa mkataba sahihi. + +## Kujenga kutoka mwanzo + +```python +lex = sil_lift.Lexicon(producer="my-script 1.0") +entry = sil_lift.Entry(id="hello", guid="...") +entry.lexical_unit["en"] = "hello" +sense = sil_lift.Sense() +sense.glosses.append(sil_lift.Form("fr", sil_lift.Text(["bonjour"]))) +entry.senses.append(sense) +lex.entries.append(entry) +lex.save("new.lift") +``` + +## Upangaji rasmi + +```python +lex.sort() # maingizo kwa (guid, id); vipimo/ufafanuzi wa uwanja kwa id/tag +lex.save() # maingizo yasiyoguswa yanabaki na baiti zao halisi, katika mpangilio mpya + +sil_lift.canonicalize("in.lift", "out.lift") # imepangwa tena kikamilifu, tayari kwa tofauti +``` + +Tazama pia: [Mfano uliofanyiwa kazi: kuhariri kwa wingi tafsiri fupi](bulk-edit-glosses.md). diff --git a/docs/sw/guides/validate.md b/docs/sw/guides/validate.md new file mode 100644 index 0000000..9c2fb9a --- /dev/null +++ b/docs/sw/guides/validate.md @@ -0,0 +1,36 @@ +# Thibitisha + +Uthibitishaji daima ni wazi — kupakia na kuhifadhi kamwe havithibitishi kwa njia fiche. + +```python +import sil_lift + +# Exhaustive: mtiririko wa vigezo (schema + tabaka za semantiki). +for problem in sil_lift.iter_problems("dictionary.lift"): + print(problem) + # kosa [dangling-ref] dictionary.lift:88 (entry apu): ref 'nope' inalingana ... + +# Fail-fast: inasababisha LiftValidationError kwenye tatizo la kwanza la kiwango cha kosa. +sil_lift.validate_file("dictionary.lift") + +# Hali ya kumbukumbu (inayosafirishwa kwanza — gharama iliyoorodheshwa kwa kamusi kubwa): +lex = sil_lift.load("dictionary.lift") +problems = list(lex.iter_problems()) +``` + +Kila `Problem` ina `level` (`"error"`/`"warning"`), `code` thabiti, `message`, na anwani: `file`, `entry_id`, `guid`, `line`. + +## Tabaka + +1. **RELAX NG** dhidi ya sarufi ya LIFT 0.13 (iliyotolewa kutoka lift-standard — nakala inayofanana byte kwa byte iliyowekwa katika kifurushi hiki). +2. **Rangi za schema** — `lift-ranges-0.13.rng` ya mradi huu — juu ya kila kiambatisho cha `.lift-ranges` kinachofuatiliwa. +3. **Ukaguzi wa semantiki** ambao sarufi haiwezi kuonyesha: `duplicate-guid`, `dangling-ref`, `range-parent`, `undefined-range-value`, `duplicate-form-lang`, `missing-media`. + +## Matokeo halisi ya FieldWorks (FLEx) + +FieldWorks kwa utaratibu huandika baadhi ya maudhui ambayo zana kali hupinga. Hapa kuna sera ya sil-lift, ili kamusi halisi ziwe na manufaa: + +- Viungo vya `file://C:/...` (URI zisizofaa) huripotiwa kama **maonyo** (`uri-not-rfc`), si makosa ya skema — mhakiki wa C# haujawahi kuzikataa. +- Watoto waliopangwa kisheria (kwa mfano `field, note, field, note`) hawapati alama, hivyo kuepuka matokeo ya uongo chanya katika libxml2. +- Thamani za masafa zinalinganishwa chini ya usawa wa Unicode NFC — FLEx huandika `.lift` katika NFC lakini `.lift-ranges` katika NFD ndani ya usafirishaji uleule. +- Nyongeza za `trait`/`field` za FLEx ndani ya `range-element` zinaripotiwa (makosa ya schema dhidi ya schema ya rangi): ni upotovu halisi wa vipimo. diff --git a/docs/sw/index.md b/docs/sw/index.md new file mode 100644 index 0000000..a1f114e --- /dev/null +++ b/docs/sw/index.md @@ -0,0 +1,32 @@ +# sil-lift + +Maktaba ya Python kwa [LIFT](https://github.com/sillsdev/lift-standard) (Lexicon Interchange FormaT) 0.13: kusoma/kuandika bila kupoteza data ya folda ya LIFT (`.lift` + `.lift-ranges` + marejeleo ya media), uthibitishaji wa schema na semantiki, na upangaji sahihi — na API za mtiririko kwa kamusi kubwa. + +Hali: kabla ya kutolewa, inatengenezwa. + +## Sakinisha + +Kutoka [PyPI](https://pypi.org/project/sil-lift/): + +``` +pip install sil-lift # maktaba + amri ya sil-lift +``` + +Inahitaji Python 3.11 na zaidi. Utegemezi pekee wakati wa utekelezaji ni lxml. + +## Ziara ya sekunde 30 + +```python +import sil_lift + +lex = sil_lift.load("thesaurus.lift") # inafuatilia .lift-ranges na wenza pia + +for entry in lex.entries: + if "en" not in entry.gloss_langs(): + print(entry.id, str(entry.lexical_unit.get("seh") or "")) + +entry = lex.find(guid="0f5a9c3e-...") # au lex.find(id="hoofd_a1b2") +entry.senses[0].definition["en"] = "head (anatomy)" + +lex.save() # entries zisizoguswa ni sawa kwa baiti; entry iliyohaririwa imehifadhiwa tena +``` diff --git a/docs/sw/reference.md b/docs/sw/reference.md new file mode 100644 index 0000000..b41d4b5 --- /dev/null +++ b/docs/sw/reference.md @@ -0,0 +1,9 @@ +# Rejea ya API + +API ya umma ni hasa kile `sil_lift` inachore-eksporti katika ngazi ya juu; moduli zilizo na viambishi awali vya 'underscore' ni maelezo ya utekelezaji. + +::: sil_lift +options: +show_root_heading: false +show_source: false +members_order: source diff --git a/docs/zh/csharp-differences.md b/docs/zh/csharp-differences.md new file mode 100644 index 0000000..9e8ab57 --- /dev/null +++ b/docs/zh/csharp-differences.md @@ -0,0 +1,52 @@ +# 与 C# 库的区别 + +sil-lift 与 SIL 的 C# LIFT 工具集大致类似——主要包括 [libpalaso](https://github.com/sillsdev/libpalaso) 中的 `SIL.Lift`(解析器、验证器、迁移器、`LiftSorter`)、同一仓库中的 `SIL.DictionaryServices`(该仓库中的 `LexEntry`/`LexSense` 模型,带有专用的 LIFT 读写器,被 The Combine 和 WeSay 所采用),以及 [Chorus](https://github.com/sillsdev/chorus) 中的 LIFT 处理程序。 这是一个全新的实现,而不是移植。 本页总结了行为在哪些方面存在有意差异。 + +## 范围 + +| 能力 | C# 库 | sil-lift | +| ------- | ------------------------------------------------- | ---------------------------------------- | +| LIFT 版本 | 0.10–0.13(内置迁移功能) | **仅限 0.13 版**;旧版本将被明确拒绝 | +| 版本迁移 | `Migrator`(XSLT 链) | 无 — 对于一次性升级,请使用 lift-standard 中的 XSLT 文件 | +| 三向合并/同步 | 合唱 | 超出范围 | +| 验证 | 仅限 RELAX NG(`Validator`) | RELAX NG + 范围模式 + 语义检查 | +| 流媒体 | 内部条目级别的解析 | 公共 `open_reader` / `open_writer` API | + +## API 结构 + +`SIL.Lift` 的解析器采用回调驱动模式(`ILexiconMerger`):它会将解析事件推送给消费者。 sil-lift 反而返回一个普通的对象图——每个 LIFT 元素对应一个带类型的数据类——因为 Python 脚本开发者需要的是对象,而不是回调函数。 `SIL.DictionaryServices` 确实在 `SIL.Lift`之上构建了一个 `LexEntry`/`LexSense` 对象模型,但作为应用程序模型,它仅代表这些应用程序所使用的结构——因此,通过它进行重新序列化无法像 sil-lift 的 LIFT 残留处理和字节保真度那样保留模型之外的内容(详见下文)。 流式处理 API 返回的是_相同的_ `Entry` 类型,因此无需学习第二个精简版模型。 + +## 往返保真度 + +最显著的刻意差异。 使用 `SIL.Lift` 保存时,会将整个文档重新序列化。 sil-lift 保证: + +- 未发生更改的文档在存储时**字节完全一致**,并且 +- 即使其他条目发生变化,未被修改的条目仍会保留其原始字节内容——这正是 Chorus 采用的、针对每个条目自动应用的字节分块机制。 + +请参阅[富达保证](fidelity.md)。 + +## 验证 + +C# 的 `Validator` 会执行一次 RELAX NG 验证,并将首次检测到的错误以字符串形式返回。 sil-lift 报告了一个结构化的 `Problem` 流,其中每个 `Problem` 都包含其相关的文件、条目和行,且其模式层在三个地方有意进行了差异化处理: + +- **无效的 URI 属于警告,而非错误。** C# RELAX NG 引擎从未强制执行 `anyURI` 数据类型,因此 FieldWorks (FLEx) 多年来一直将 `file://C:/...` 格式的 href 写入实际词汇表中。 如果拒绝这些文件,几乎所有 FLEx 导出文件都会被标记。 +- **Schematron 规则已强制执行**(作为语义检查):LIFT 语法中重复的表单语言和类似的协同约束,在 C# 和原生 lxml 验证中均被静默忽略。 +- **跨文件比较已进行 Unicode 标准化**,因为 FLEx 将 `.lift` 文件以 NFC 格式写入,而配套的 `.lift-ranges` 文件则以 NFD 格式写入。 + +sil-lift 还会根据独立范围文档的模式(该模式与基础 LIFT 语法一同由 `lift-standard` 提供)来验证已加载词汇表的 `.lift-ranges` 伴随文件 —— 每次验证 `.lift` 文件时,都会检查所有被追踪的外部范围文件 —— 而 C# 领域中则不存在此类模式(或检查)。 (目前尚无独立于 `.lift` 文件之外、专门用于验证 `.lift-ranges` 文件的入口点。) + +## 规范排序 + +`Lexicon.sort()` 遵循 `LiftSorter` 的核心规则(条目按不区分大小写的 GUID 排序;范围和范围元素按 ID 排序;标头字段定义按标签排序;词义按文件顺序排列;`` 内的空格绝不被修改),但有三点不同: + +- 没有 GUID 的条目将按 ID 进行确定性排序(`LiftSorter` 假设存在 GUID); +- 排序与区域设置无关(采用纯大写/小写转换后的码点,而非 .NET 的“不变文化”排序规则); +- 同类列表(如笔记、关系和表单)会保留其文档顺序,而非按键值重新排序——分组本身已是确定性的,重新排序只会增加差异噪声。 + +规范代码库中的 `canonicalizeLift.xsl` 完全未被使用:它会压缩词法文本中的空白(破坏性操作),且每次运行生成的 ID 都不相同。 + +## 未结转 + +- WeSay 特有的便捷功能(围绕 LIFT 文件的仪表盘/配置管理)。 +- `SynchronicMerger`(Chorus 更新合并)——字节分块的理念在保真层中得以延续,但合并操作已不复存在。 +- LDML 书写系统解析:`WritingSystems/` 目录下的文件将被视为不透明的文件夹内容。 diff --git a/docs/zh/fidelity.md b/docs/zh/fidelity.md new file mode 100644 index 0000000..35648d5 --- /dev/null +++ b/docs/zh/fidelity.md @@ -0,0 +1,33 @@ +# 富达保证 + +LIFT 是一种_交换_格式,因此第一条规则是:**切勿省略你不理解的内容**。 `sil-lift` 的契约,在每次运行时均由测试套件进行验证(包括语料库文件以及基于属性的生成): + +## 阅读 + +任何格式正确的 LIFT 0.13 文档都能加载——即使其中包含不符合模式的内容。 模型未定义的内容将作为_LIFT残余_存储在最近节点的非透明`Extras`容器中——这是FieldWorks对这一概念的称谓,它将这些内容存储在`LiftResidue`字段中:未知属性和元素、XML注释和处理指令、散落的文本以及格式错误的类型化属性 (一个无效的日期将作为原始字符串保留在 `Extras` 中;其类型化字段为 `None`)。 + +## 保存未作修改的文档 + +`load()` → `save()`(未进行任何编辑)将生成**字节完全相同的输出**——不进行重新格式化、不进行重新转义、不重新排序,并包含字节序标记和XML声明。 目前没有规范化列表:恒等式是精确的。 + +例外情况(写入器将回退到完整的规范序列化,这种序列化在语义上完整,但无法保留字节顺序): + +- 源编码与 ASCII 不兼容(非 UTF-8/US-ASCII),或者 +- 源文件中包含 DOCTYPE 声明,或者 +- 字节扫描器和解析器对文档的顶级结构存在分歧——例如,出现了一个不符合规范的第二个 `
`,解析器只保留了其中一个(扫描器则故意采取保守策略:一旦有疑问,它就完全不捕获任何源代码字节),或者 +- 源代码是在内存中构建的,而不是从文件中加载的。 + +## 保存已编辑的文档 + +- \*\*未被修改的条目将按其原始字节内容原样输出。\*\*如果条目的模型对象自解析以来有任何部分发生变化(通过规范序列化快照检测,而非通过“已修改”标志),则该条目被视为已被修改。 +- **被修改的条目将按照规范进行完全重新序列化**:UTF-8 编码,混合内容_外部_采用双空格缩进(`` 和 `` 内部的空格绝不更改),每个元素均按文档规定进行子元素分组(例如条目: 词汇单元、引用、发音、变体、词义、注释、关联、词源、标注、特征、领域),固定的属性顺序,日期采用 ISO-8601 格式(`Z` 表示 UTC)。 所有残留项都会被重新发出;其位置将恢复到原始子索引处,并限定在新的子列表范围内(这是一种近似处理——只有未被修改的条目才能保证精确的字节位置)。 +- 添加、删除或重新排序条目会重新序列化文档结构,但仍会原样输出每个未更改条目的字节。 + +!!! note ""此处的规范" 与任何其他规范XML均无关" + 本页所指的“规范形式”是指 `sil-lift` 自身文档中记载的形态,如上文项目符号中所述。 这与 W3C 的规范 XML(C14N)流程无关。 这与 `SIL.Core` 中的 `CanonicalXmlSettings` 类无关。 + +## 已知的近似值(仅限已访问节点) + +- `` 运行块`内部`的注释会被保留,但会移至运行块旁边,而非保留在原有的精确字符偏移位置。 +- 在已编辑元素内的交叉型子元素顺序会被规范化为规范分组(LIFT 模式中的 `interleave` 使得该顺序在语义上不具意义)。 +- 一个虽然存在但不包含任何内容(既无形式,也无残留)的多文本元素——例如 `` ——不会被重新输出。 该模型将这些字段表示为始终存在的 `Multitext`(包括 `lexical-unit`、`citation`、`definition`、关系的 `usage` 以及 `label` / `abbrev` / `description`,这些字段分别位于 URL 引用、范围、范围元素以及标题中),因此解析后,空字段与缺失字段无法区分。 没有任何语义上的损失。 diff --git a/docs/zh/guides/build-export.md b/docs/zh/guides/build-export.md new file mode 100644 index 0000000..1e70529 --- /dev/null +++ b/docs/zh/guides/build-export.md @@ -0,0 +1,168 @@ +# 示例:从零开始构建 LIFT 导出 + +如果你正在将另一个应用程序的数据导出为 LIFT 格式——这正是 [生成符合规范的 LIFT](lift-export-interop.md) 背后的任务——`sil-lift` 可以逐个构建文档对象并将其序列化,而无需手动生成 XML。 本文将逐步演示一个脚本,该脚本利用真实词典所包含的各项要素(多种书写系统、发音、带例句的词义、插图、语义领域特征以及应用程序特定字段)来构建词条,将受控词汇写入 `.lift-ranges` 伴生文件,进行验证并保存。 + +## 剧本 + +```python +from pathlib import Path + +import sil_lift + +lex = sil_lift.Lexicon(producer="my-exporter") + +# 一个条目,基于源模型构建。 +entry = sil_lift.Entry(id="kanga", guid="6b9e7c2a-3f4d-4a1b-8c5e-2d9f0a1b2c3d") +entry.lexical_unit["seh"] = "nkhuku" +entry.lexical_unit["pt"] = "galinha" + +pron = sil_lift.Pronunciation() +pron.forms["en"] = "Speaker: Ana" # Combine 的说话人标签约定 +pron.media.append(sil_lift.URLRef(href="audio/nkhuku.wav")) +entry.pronunciations.append(pron) + +sense = sil_lift.Sense(id="kanga_s1") +sense.grammatical_info = sil_lift.GrammaticalInfo(value="Noun") +sense.glosses.append(sil_lift.Form(lang="en", text=sil_lift.Text(["chicken"]))) +sense.definition["en"] = "a domestic fowl kept for its eggs and meat" + +example = sil_lift.Example() +example.forms["seh"] = "Ndinafuna nkhuku." +translation = sil_lift.Translation() +translation.forms["en"] = "I want a chicken." +example.translations.append(translation) +sense.examples.append(example) + +photo = sil_lift.URLRef(href="pictures/hen.jpg") +photo.label["en"] = "A hen" +sense.illustrations.append(photo) + +sense.traits.append(sil_lift.Trait(name="semantic-domain-ddp4", value="1.6.1.2")) + +scientific = sil_lift.Field(type="scientific-name") # 一个应用程序专用的额外字段 +scientific.content["en"] = "Gallus gallus domesticus" +sense.fields.append(scientific) + +entry.senses.append(sense) +lex.entries.append(entry) + +# 该词条所引用的受控词汇表,位于配套的 .lift-ranges 中。 +ranges = sil_lift.RangesFile() +ranges.add_range("grammatical-info").add_element("Noun").label["en"] = "noun" +ranges.add_range("semantic-domain-ddp4").add_element("1.6.1.2").label["en"] = "Bird" +lex.add_ranges_file(ranges, href="birds.lift-ranges") + +# 在写入磁盘之前,验证 save() 会写入什么内容。 +problems = list(lex.iter_problems()) +print(f"validation: {len(problems)} 个问题") + +out = Path("export") +out.mkdir(exist_ok=True) +lex.save(out / "birds.lift") +print("=== birds.lift ===") +print((out / "birds.lift").read_text(encoding="utf-8"), end="") +print("=== birds.lift-ranges ===") +print((out / "birds.lift-ranges").read_text(encoding="utf-8"), end="") +``` + +## 其产出 + +`验证结果:0 个问题`,然后是 `.lift` 及其配套代码的并列展示: + +``` +=== birds.lift === + + +
+ + + + +
+ + + + nkhuku + +
+ galinha +
+
+ +
+ Speaker: Ana +
+ +
+ + + + chicken + + +
+ a domestic fowl kept for its eggs and meat +
+
+ +
+ Ndinafuna nkhuku. +
+ +
+ I want a chicken. +
+
+
+ + + + + +
+ Gallus gallus domesticus +
+
+
+
+
+=== birds.lift-ranges === + + + + + + + + + + + + + +``` + +## API说明 + +- 多文本字段(`lexical_unit`、`definition`、`Form`/`URLRef` 的标签、`Field` 的内容等) 通过映射接口,为每个书写系统取一条字符串:`entry.lexical_unit["seh"] = "nkhuku"` 会添加一个 `
`。 一个以语言代码为键对字符串进行索引的源模型可以直接映射到此处。 +- `RangesFile.add_range()` / `Range.add_element()` 用于构建受控词汇表,而 `Lexicon.add_ranges_file(ranges, href=...)` 则关联相应的范围文件并添加 `` 引用——这样,条目的 `` 和 `` 就能解析为您定义的范围。 +- `URLRef` 由 href 以及可选的标题/标签多文本组成——既适用于 ``(音频),也适用于 ``(照片)。 此处的发音遵循“The Combine”的惯例,即采用读作“ ”的`en`形式。 +- 不包含原生 LIFT 回家行程的应用程序专用数据,格式为 ``(或 ``):FieldWorks 会将其识别为自定义字段,而 The Combine 会保留这些数据。 +- 为每个条目分配一个真实且稳定的 `guid`(例如通过 `uuid.uuid4()` 生成,并在不同导出之间复用)——这样,后续重新导入时会就地更新该条目,而非创建重复条目。 `sil-lift validate --require-ids` 会强制执行此要求。 +- `lex.iter_problems()` 会在任何数据写入磁盘之前,对内存中的文档(即 `save()` 会写入的内容)进行验证;此时该文档是干净的。 由于词汇表目前还没有文件夹,因此会跳过媒体存在性检查和关联链接检查——待音频和照片文件就位后,请对保存的输出结果运行 [`sil-lift validate`](cli.md)(或使用 `--no-check-media` 选项)。 + +## 包装 + +`lex.save("export/birds.lift")` 会生成文件夹结构(`.lift` 和 `.lift-ranges` 并列存放)。 若要生成一个可被 FieldWorks 和 The Combine 直接导入的单个压缩包,请改用 `lex.save_zip("birds.zip")` —— 参见 [生成符合规范的 LIFT 文件](lift-export-interop.md)。 diff --git a/docs/zh/guides/bulk-edit-glosses.md b/docs/zh/guides/bulk-edit-glosses.md new file mode 100644 index 0000000..613b837 --- /dev/null +++ b/docs/zh/guides/bulk-edit-glosses.md @@ -0,0 +1,79 @@ +# 示例:批量编辑释义 + +一项常见的维护任务:将词汇表中所有英语释义的拼写统一为标准形式(英式英语 → 美式英语,或反之),同时不影响文件中的其他内容。 本文将逐步演示一个脚本,该脚本负责加载、编辑、验证和保存数据,从而展示编辑 API 与保真度保证如何协同工作。 + +## 剧本 + +```python +import sys + +import sil_lift + +path = "dictionary.lift" +lex = sil_lift.load(path) + + +def iter_senses(senses): + """返回每个词义,包括子词义(递归)。”"" + for sense in senses: + yield sense + yield from iter_senses(sense.subsenses) + + +edited_glosses = 0 + +for entry in lex.entries: + for sense in iter_senses(entry.senses): + for gloss in sense.glosses: + if gloss.lang != "en": + continue + old = str(gloss.text) + new = old.replace("colour", "color") + if new != old: + gloss.text = sil_lift.Text([new]) + edited_glosses += 1 + +changed = lex.changed_entries() + +errors = [p for p in lex.iter_problems() if p.level == "error"] +if errors: + for problem in errors: + print(problem) + sys.exit(f"中止:{len(errors)} 个验证错误,未保存任何内容") + +lex.save() +print(f"已编辑 {edited_glosses} {len(changed)} 个条目中的释义") +``` + +有几点值得注意: + +- `Sense.subsenses` 本身是一个 `list[Sense]`,因此 `iter_senses` 会递归遍历它——如果批量编辑操作仅遍历 `entry.senses`,则会无提示地跳过任何嵌套在子义项下的释义。 +- `gloss.text` 是一个 `Text` 对象,而不是普通的字符串:`str(gloss.text)` 会将其扁平化以便进行匹配,而替换后的内容会通过 `sil_lift.Text([new])` 写回,而不是直接修改原字符串。 +- `lex.changed_entries()` 会报告哪些条目与加载的文件存在差异。 由于条目的摘要涵盖了其整个子树,因此对嵌套的子义项进行编辑时,系统会报告包含该子义项的条目。 + - 它会比较序列化的内容,因此将某个字段赋值为其原有的值时,系统不会报告该操作。 + - 它仅报告内容变更;`lex.added_entries()` 和 `lex.removed_entries()` 涵盖自加载以来新增或删除的条目。 + - 它返回条目本身,不受 `id` 重复或缺失的影响(LIFT 允许这种情况)。 + - 作为一种计数方式,只有在有可比较对象的情况下才有意义。 当字节扫描器无法读取源数据时(例如编码与 ASCII 不兼容,或者扫描器与解析器之间存在分歧),由于缺乏基准,`changed_entries()` 会报告 _所有_ 条目。 对于写保护而言,这确实是一个诚实的答案,因为在这种情况下 `save()` 会重新序列化整个文件,但这意味着计数值代表的是词汇表的大小,而不是编辑内容的大小。 +- `lex.changes()` 用于判断文档是否_有任何_更改。 它不仅涵盖了条目,还包括页眉、根元素以及所有 `.lift-ranges` 关联元素。 + - 只有当 `save()` 能重现源字节时,该条件才会为假,因此 `if not lex.changes(): ...` 才是跳过不必要写入的正确方法。 该保证是单向的:对于本应被重写的文档,它绝不会报告“无内容可写”;而那些会触发完整重新序列化的更改,即使最终结果与原始字节完全一致,仍会被报告。 + - 它比较的是内容,而非目标路径,因此仅应将其用于就地保存:`lex.save(some_other_dir / "dictionary.lift")` 会将文档及其相关文件写入一个目前尚无内容的目录,无论内容是否发生变化。 + - 这是一种保护机制,而非加速手段——调用该函数会处理每个条目,这与 `save()` 决定哪些源字节可以复用的工作完全相同,因此你跳过的只是写入操作本身(文件修改时间保持不变,不会产生虚假差异),而非决策过程。 +- 内存验证(`lex.iter_problems()`)会先将编辑后的状态序列化,因此能在将任何内容写入磁盘之前,准确反映编辑后的状态。 一旦遇到任何 `"error"` 级别的 `Problem` 就会中止处理——警告信息将保留给调用方自行决定如何处理——这意味着错误的编辑操作永远不会进入 `save()` 方法。 + +值得这样处理的不仅仅是光泽。 相同的 `Multitext` 映射规则同样适用于定义以及条目或词义中的所有其他多语言字段: + +```python +sense.definition["en"] = "事物的颜色" +``` + +## 运行它 + +针对一个包含简短词汇表的小型词库进行比对,其中该词的释义和子义释义均显示为“colour”: + +``` +编辑了 1 个条目中的 2 个释义 +``` + +## 忠诚的回报 + +该保证针对每个_条目_:模型未发生变化的条目,其输出与读取时的内容在**字节级**完全一致,且只有您实际修改过的条目才会被重新序列化。 在上面的处理过程中,有一个条目的注释被编辑了——文件中的其他所有条目都保留了原有的字节内容。 (请注意粒度:编辑条目中的任何部分都会导致整个条目重新序列化,包括未被修改的同级释义。) 因此,在一部包含50,000条目词典中编辑一条释义,生成的差异文件仅涉及一条条目,而非重新格式化的文件。 具体合同条款请参见[富达保证](../fidelity.md)。 diff --git a/docs/zh/guides/cli.md b/docs/zh/guides/cli.md new file mode 100644 index 0000000..ed3cecf --- /dev/null +++ b/docs/zh/guides/cli.md @@ -0,0 +1,70 @@ +# 命令行 + +安装该包(`pip install sil-lift`)时,还会一并安装 `sil-lift` 命令——这是一个遵循 LiftTools 理念的受支持工具,随包附带(对于 `validate` 而言,它还是该库 API 的一个示例)。 + +``` +sil-lift 验证 PATH [--format {text,json}] [--strict] [--no-check-media] [--require-ids] + 所有问题,包含文件/条目/行信息;出现错误时退出并返回 1 +sil-lift stats PATH [--format {text,json}] + 条目/语义/语言计数(流式处理;任意大小) +sil-lift sort PATH [-o OUT] 按规范排序、可进行差异比较的副本(默认:就地操作) +sil-lift check-media PATH 缺失和孤立媒体报告;若存在缺失则退出并返回 1 +sil-lift export PATH [-o OUT] [--langs L] [--tsv] + 将每个叶片感测单元(子感测单元已扁平化)按行导出至 CSV/TSV 文件(流式输出) +``` + +`--format json` 会将单个 JSON 对象写入标准输出(且不输出其他内容),供持续集成(CI)和自动化流程使用;请参阅下例中的数据结构。 `--strict` 将警告视为错误,若发现任何警告则返回 1 —— 使用该选项可确保构建仅在完全没有警告的情况下通过,而非仅在没有错误的情况下通过。 `--no-check-media` 会跳过文件系统的媒体存在性检查(从而抑制 `missing-media` 错误提示),这在验证刚生成的导出文件时非常有用,尤其是当音频/照片文件位于其他位置而非同一文件夹内时。 `--require-ids` 还会对任何缺少 `guid` 的条目或缺少 `id` 的语义返回错误(`missing-id` 错误)——这比 LIFT 更严格,适用于通过稳定 ID 重新导入的工作流。 将 `-` 作为路径参数传递时,系统将从标准输入(stdin)读取文档(通过管道传递的文档没有文件夹,因此其配套的 `.lift-ranges` 文件和媒体资源不会被解析)。 `stats` 同样支持 `--format json` 选项,将计数结果以单个 JSON 对象的形式输出。 + +!!! note + `validate` 的退出代码和 `--format json` 模式是一种受支持的自动化接口:两者均经过测试验证,且仅在遵循 SemVer 规范的情况下才会发生变更。 + +`sort` 仅重写 `.lift` 文件;配套的 `.lift-ranges` 文件则保持不变 +(请使用 `RangesFile` API 单独对这些文件进行排序)。 + +`validate`、`stats`、`check-media` 和 `export` 还支持接收压缩的 LIFT 包(以任何一种布局格式的 `.zip` 文件——文件位于归档根目录下,或嵌套在某个顶级文件夹下);该包会在命令执行完毕后解压到临时目录,并被自动删除。 流式处理命令 `stats` 和 `export` 仅提取 `.lift` 文件本身,因此在处理媒体资源较多的包时,其开销较低;而 `validate` 和 `check-media` 则需要整个文件夹,并将其全部提取出来。 + +示例: + +``` +$ sil-lift validate dictionary.lift +错误 [dangling-ref] dictionary.lift:88(条目 apu):引用 'nope' 未匹配任何条目 ID/GUID 或词义 ID +警告 [uri-not-rfc] dictionary.lift:6: :将 Windows 驱动器盘符用作 URI 权威部分(FLEx 风格的 file://C:/) +1 个错误,1 个警告 + +$ sil-lift validate dictionary.lift --format json +{ + "problems": [ + { + "level": "error", + "code": "dangling-ref", + "message": "引用 'nope' 未匹配任何条目 ID/GUID 或语义 ID", + "file": "dictionary.lift", + "entry_id": "apu", + "guid": null, + "line": 88 + }, + { + "level": "warning", + "code": "uri-not-rfc", + "message": ": 将 Windows 驱动器盘符用作 URI 权威部分(FLEx 风格的 file://C:/)", + "file": "dictionary.lift", + "entry_id": null, + "guid": null, + "line": 6 + } + ], + "summary": { + "errors": 1, + "warnings": 1 + } +} + +$ sil-lift stats sango.lift +条目数: 3507 +词义数: 4541 +... + +$ sil-lift export dictionary.lift --langs en,fr -o dictionary.csv +``` + +退出代码:`0` 成功(允许出现警告,除非启用了 `--strict` 选项),`1` 发现问题(验证错误/媒体文件缺失/在启用 `--strict` 选项时出现的警告),`2` 输入不可读。 diff --git a/docs/zh/guides/folder-media.md b/docs/zh/guides/folder-media.md new file mode 100644 index 0000000..860793f --- /dev/null +++ b/docs/zh/guides/folder-media.md @@ -0,0 +1,41 @@ +# LIFT 文件夹:范围与媒体 + +一个 LIFT 词汇表通常是一个 _文件夹_:包含 `.lift` 文件、一个或多个 `.lift-ranges` 辅助文件(sidecar 文件),以及 `audio/` / `pictures/` 中的媒体文件。 + +## 范围 + +```python +lex = sil_lift.load("dictionary.lift") # 自动追踪伴随对象 + +lex.ranges_files # {Path(...): RangesFile} +lex.all_ranges() # 合并后的 {id: Range} 视图 +lex.all_ranges()["grammatical-info"].elements +``` + +Companion discovery 处理现实情况:使用指向现有文件的 `range/@href`; FieldWorks 中悬空的绝对 `file://C:/...` href 会回退到 href 的基名,并将其附加在 `.lift` 之后;而常规的 `.lift-ranges` 同级文件,即使没有任何引用,也会被识别出来。 + +`lex.save()` 会将 `.lift` 以及所有被追踪的伴生类一起写入。 对 `RangesFile` 进行的修改将保存回该文件;未修改的范围将保留其精确的字节数据。 独立使用: + +```python +ranges = sil_lift.RangesFile.load("dictionary.lift-ranges") +ranges.find("grammatical-info") +ranges.sort() +ranges.save() +``` + +向 `load()` 传递 `resolve_ranges=False` 参数,以跳过伴侣节点发现。 + +## 媒体 + +```python +for ref in lex.media_refs(): # 所有 + print(ref.kind, ref.href, ref.entry_id) + +lex.missing_media() # 文件不存在的引用 +``` + +解析遵循常规布局:对相对 href 进行原样检查(反斜杠已规范化——WeSay 写的是 `pictures\photo with space.png`),并检查其是否位于 `audio/`(用于发音媒体)或 `pictures/`(用于插图)目录下。 无法验证远程/绝对 href,因此会跳过这些链接。 + +## 其他文件夹内容 + +LIFT 文件夹通常包含 sil-lift 未建模的文件——例如 `WritingSystems/` 下的书写系统 LDML、`consent/` 下的 The Combine 发言人同意书的音频/图像文件等; `load()`/`save()` 不会对这些文件进行任何操作,而 [`Lexicon.save_zip()`](lift-export-interop.md) 在打包文件夹时会原样保留这些文件。 diff --git a/docs/zh/guides/large-files.md b/docs/zh/guides/large-files.md new file mode 100644 index 0000000..dc3a619 --- /dev/null +++ b/docs/zh/guides/large-files.md @@ -0,0 +1,27 @@ +# 大文件(流式传输) + +`load()` 会构建整个对象图。 对于大小达数百MB的词典,流式处理 API 会在有限的内存中逐条处理条目——由于采用的是相同的 `Entry` 类型,因此针对一种模式编写的代码在另一种模式下同样适用。 + +```python +import sil_lift + +with sil_lift.open_reader("big.lift") as reader: + header = reader.header # 提前解析(位于条目之前) + for entry in reader: # 惰性 Iterator[Entry] + ... +``` + +```python +使用 sil_lift.open_reader("big.lift") 作为读取器,sil_lift.open_writer( + "out.lift", header=reader.header, producer="my-script" +) 作为写入器: + for entry in reader: + if not entry.date_deleted: # 例如:删除坟墓石 + writer.write(entry) +``` + +注: + +- 该写入器的输出结果与全文档规范序列化器针对相同内容生成的结果完全一致——这两种模式的结果始终保持一致。 +- 流式传输模式不复用源字节:输出始终是规范的。 根级 LIFT 残留数据——即 `` 中条目之间的注释以及不符合模式的属性——不会被保留;条目和头部信息是完整的,其中包含残留数据。 +- 如果 `open_writer` 代码块内部抛出异常,该文件将被标记为未终止(即没有关闭的 ``)——一个只写了一半的词汇表绝不能看起来像是完整的。 diff --git a/docs/zh/guides/lift-export-interop.md b/docs/zh/guides/lift-export-interop.md new file mode 100644 index 0000000..a5a9d11 --- /dev/null +++ b/docs/zh/guides/lift-export-interop.md @@ -0,0 +1,112 @@ +# 生成符合规范的 LIFT + +本指南适用于任何编写 LIFT _导出器_ 的开发者——即使用任何编程语言编写、将其他应用程序的数据模型转换为 LIFT 0.13 格式的代码。 在该工作中,`sil-lift` 承担着双重作用:一是作为符合性检查机制,既验证输出是否符合模式规范,又处理模式无法表达的语义;二是作为输出必须遵循的形状和文本规则的参考标准。 + +编写 LIFT 要比解析它容易得多:导出器只会输出其自身模型生成的子集结构,而无需处理完整规范中的可选项。 难点在于细节——`.lift-ranges` 伴生组件、针对各书写系统的文本、稳定的标识符以及 XML 转义——而下文中的检查项正是针对这些细节的。 + +## 压缩包 + +LIFT 通常以单个 `.zip` 文件的形式进行传输——FieldWorks 和 The Combine 都采用这种方式进行导入和导出——因此 `sil-lift` 可以直接读取和写入压缩包,无论采用生态系统中哪种布局:文件位于归档根目录下,还是嵌套在某个顶级文件夹之下。 + +- **说明:** `sil_lift.load("package.zip")` 会将文件解压到临时目录中,定位唯一的 `.lift` 文件,并将其加载(相关文件和媒体资源将按常规方式解析)。 + - `validate`、`stats`、`check-media` 和 `export` 命令行命令也支持 `.zip` 路径,因此下面的门控脚本可直接对该包进行处理。 + - `stats` 和 `export` 流,并仅提取 `.lift` 文件而非整个包——这样在媒体资源密集的场景下仍能保持低开销,且提取限制仅适用于 `.lift` 文件本身,而不涉及其余内容。 + - 提取操作的上限为 10 GiB 和 100,000 个成员;超过任一限制的包将因 `LiftParseError` 而被拒绝,成员路径超出提取目录范围的包同样会被拒绝。 +- **编写:** `Lexicon.save_zip("out.zip", wrap_folder="MyDict")` 会将 `.lift`、其 `.lift-ranges` 以及源文件夹中的所有其他文件(media、`WritingSystems/`、`consent/` 等)打包在一起 打包成zip文件。 + - `wrap_folder` 的默认行为是创建一个以压缩包命名的顶级文件夹(遵循 FieldWorks/Combine 的导入规范);若要生成扁平化归档,请传入 `False`。 + +`.lift` 和 `.lift-ranges` 在包内部保持字节级精确性;而 zip 容器本身无法实现字节级还原。 + +## 将输出作为符合性检查点进行验证 + +将 `sil-lift validate` 指向生成的 `.lift` 文件。 它运行 RELAX NG(同时针对 `.lift` 及其配套的 `.lift-ranges`),并执行语法无法表达的语义检查: 悬空的 `relation`/`variant` 引用、重复的 GUID、范围元素父元素的完整性、在所属范围内未定义的性状和语法信息值,以及解析后未找到对应伴侣的 `range/@href` 引用。 + +对于持续集成(CI),只要出现任何问题就应报错,并输出机器可读的检测结果: + +``` +sil-lift validate export.lift --strict --no-check-media --format json +``` + +- `--strict` 会使警告(而不仅仅是错误)导致运行失败。 +- `--no-check-media` 会跳过文件系统的媒体存在性检查;当音频/照片文件与 CI 中的 `.lift` 文件不在同一文件夹时,该检查得出的 `missing-media` 结果属于误报。 +- `--format json` 会输出一个 JSON 对象(`{"problems": [...], "summary": {...}}`),而非可读文本;其退出代码和模式构成一个受支持且符合 SemVer 规范的接口(参见 [命令行指南](cli.md))。 +- `--require-ids` 还会针对缺少 `guid` 的条目或缺少 `id` 的字段报错——这在后续重新导入时需要更新而非重复导入的情况下非常有用。 + +通过使用 `stats --format json` 对源模型进行计数验证,以防范“隐性数据丢失”(即导致平面 CSV 导出出现数据丢失的故障模式): + +``` +sil-lift 统计数据导出:export.lift --format json +``` + +它报告了`条目`、`释义`、`例句`、`媒体引用`、`语言`以及按名称划分的`特征`数量。 + +### 在不使用 Python 工具链的情况下运行 Gate + +TypeScript 或 C# 项目的持续集成(CI)可以通过内置的 GitHub Action 运行相同的检查,而无需安装 Python: + +```yaml +- uses: sillsdev/python-sil-lift@v0.1.0 + with: + path: export.lift + strict: "true" + no-check-media: "true" + format: json +``` + +或者由仓库中的 `Dockerfile` 构建的容器镜像: + +``` +docker build -t sil-lift . +docker run --rm -v "$PWD:/work" -w /work sil-lift validate export.lift --strict +``` + +## `.lift-ranges` 伴生组件 + +受控词汇表——包括词性、语义领域以及任何其他基于特征的键值对集——存储在同级的 `.lift-ranges` 文件中,并在 `
` 中引用该文件: + +```xml +
+ + + + +
+``` + +该手册收录了各系列产品的完整定义。 这些值是 ``;`parent` 构建层次结构;`label` / `abbrev` / `description` 是多文本: + +```xml + + + + + +
n
+
+
+ + + + + +
+``` + +随后,条目通过 ID 引用该值:词类的 ID 为 ``,语义领域的 ID 为 ``。 当某个值未在其范围内定义时,`sil-lift validate` 会发出警告(`undefined-range-value`);当 `parent` 不是同级 ID 时,会报错(`range-parent`)——因此请仅输出数据实际使用的范围。 另请参阅 [频段与传输介质](folder-media.md)。 + +如果你使用 Python 构建导出文件,`Lexicon.add_ranges_file()`、`RangesFile.add_range()` 和 `Range.add_element()` 会自动为你构建关联对象并添加头文件引用; `open_writer(..., ranges=...)` 则在流式路径上执行相同操作。 + +## 文本和多文本 + +LIFT 中的每条人类语言字符串都是一个 _多文本_:每个书写系统对应一个 `
`,每个 ` ` 都包含一个 ``: + +```xml + + kanga +
小鸡
+
+``` + +一个按语言代码对字符串进行索引的模型(`MultiString`、`Record` 或 `dict[str, str]`)与该模型之间存在一对一映射:每个键对应一个条目,即一个 `
`。 在单个多文本中,每种语言最多允许出现一个形式——否则,`sil-lift` 会发出 `duplicate-form-lang` 警告。 + +XML转义是唯一真正需要严格确保正确性的部分。 在元素文本中,`&`、`<`, and `>` 必须进行转义(`&`、`<`、`>`);在属性值中,引号字符也必须进行转义。 `sil-lift` 的编写者严格遵循这些规则,绝不会修改 `` 内的空白字符——它不会在此处添加任何缩进,因为那样会破坏词法数据。 如果你希望生成与源数据完全一致的输出,请复用真正的 XML 序列化器的转义处理(而不是那种会遗漏 `&` 的自制替换方案),并将 `` 内容按字节原样保留,与源数据保持完全一致。 diff --git a/docs/zh/guides/read-edit-write.md b/docs/zh/guides/read-edit-write.md new file mode 100644 index 0000000..1eda253 --- /dev/null +++ b/docs/zh/guides/read-edit-write.md @@ -0,0 +1,66 @@ +# 阅读、编辑、撰写 + +## 正在加载 + +```python +import sil_lift + +lex = sil_lift.load("dictionary.lift") +``` + +`load()` 接受任何格式正确的 LIFT **0.13** 文档——包括那些不符合模式规范的实际文件。 模型未定义的任何内容(未知元素/属性、注释)都会作为 LIFT 残余信息,无损地保存在每个节点的不透明 `extra` 字段中。 其他 LIFT 版本会抛出一个名称中包含该版本号的 `LiftParseError` 异常。 + +## 该模型 + +每个 LIFT 元素都是一个带类型的数据类:`Entry`、`Sense`、`Example`、`Pronunciation`、`Variant`、`Relation`、`Etymology`、`Reversal` 等。 多语言文本是一个 `Multitext`,其行为类似于从语言代码到 `Text` 的映射: + +```python +entry = lex.find(id="abat") + +str(entry.lexical_unit["seh"]) # "abat" +entry.lexical_unit["en"] = "grove" # 普通字符串会被强制转换 +"en" in entry.citation # False +``` + +`Text` 具有结构化特征——即由 `str` 和 `Span` 片段组成的有序列表——因为 `` 可能包含嵌套的 `` 标记。 `str(text)` 会将其转换为纯文本;这些片段保留了标记,以便进行往返转换。 + +在 LIFT 中,释义是 _Form 类型的_(每个 `` 都承载着自己的语言),因此一个语义具有 `glosses: list[Form]` 以及一个辅助函数: + +```python +sense = entry.senses[0] +sense.gloss("en") # Text | None +entry.gloss_langs() # {"en", "id"} +``` + +## 保存 + +```python +lex.save() # 保存回原始加载位置 +lex.save("elsewhere.lift") +``` + +未修改的条目将以**字节完全一致**的方式写回;完全未修改的文档从第一个字节到最后一个字节都与原文档字节完全一致。 具体合同条款请参见[富达保证](../fidelity.md)。 + +## 从零开始构建 + +```python +lex = sil_lift.Lexicon(producer="my-script 1.0") +entry = sil_lift.Entry(id="hello", guid="...") +entry.lexical_unit["en"] = "hello" +sense = sil_lift.Sense() +sense.glosses.append(sil_lift.Form("fr", sil_lift.Text(["bonjour"]))) +entry.senses.append(sense) +lex.entries.append(entry) +lex.save("new.lift") +``` + +## 规范排序 + +```python +lex.sort() # 按 (guid, id) 对条目进行排序; 按 id/tag 划分的范围/字段定义 +lex.save() # 未修改的条目保留其精确字节值,并按新顺序排列 + +sil_lift.canonicalize("in.lift", "out.lift") # 完全重新序列化,已准备好进行差异比较 +``` + +另请参阅:[示例:批量编辑释义](bulk-edit-glosses.md)。 diff --git a/docs/zh/guides/validate.md b/docs/zh/guides/validate.md new file mode 100644 index 0000000..0009ae7 --- /dev/null +++ b/docs/zh/guides/validate.md @@ -0,0 +1,36 @@ +# 验证 + +验证总是显式的——加载和保存操作绝不会进行隐式验证。 + +```python +import sil_lift + +# 穷举:一个由问题(模式层 + 语义层)组成的惰性流。 +for problem in sil_lift.iter_problems("dictionary.lift"): + print(problem) + # 错误 [悬空引用] dictionary.lift:88 (条目 apu):引用 'nope' 与 ... 匹配 + +# 快速失败:在遇到第一个错误级别的问题时抛出 LiftValidationError。 +sil_lift.validate_file("dictionary.lift") + +# 内存状态(先进行序列化——这是大型词典中已记录的开销): +lex = sil_lift.load("dictionary.lift") +problems = list(lex.iter_problems()) +``` + +每个 `Problem` 包含 `level`(`"error"`/`"warning"`)、一个固定的 `code`、`message` 以及一个地址:`file`、`entry_id`、`guid`、`line`。 + +## 各层 + +1. **RELAX NG** 基于 LIFT 0.13 语法(从 lift-standard 引入——这是一个与本包中提交的版本字节级完全一致的副本)。 +2. **范围模式** —— 本项目的 `lift-ranges-0.13.rng` —— 适用于所有被追踪的 `.lift-ranges` 伴生类。 +3. **语义检查**:语法无法表达以下情况:`duplicate-guid`、`dangling-ref`、`range-parent`、`undefined-range-value`、`duplicate-form-lang`、`missing-media`。 + +## FieldWorks(FLEx)的实际应用输出 + +FieldWorks 会系统性地生成一些会被严格工具过滤掉的内容。 以下是 sil-lift 的政策,旨在确保真实的词汇表能够发挥实际作用: + +- `file://C:/...` 格式的 href(无效的 URI)会被报告为 **警告**(`uri-not-rfc`),而非模式错误——C# 验证器从未拒绝过此类 URI。 +- 合法交错的子元素(例如,某种意义上的 `field, note, field, note`)**不会**被标记,以此规避 libxml2 中的误报问题。 +- 范围值的比较是在 Unicode NFC 规范化下进行的——FLEx 在同一导出文件中将 `.lift` 写为 NFC 格式,而将 `.lift-ranges` 写为 NFD 格式。 +- FLEx 在 `range-element` 中的 `trait`/`field` 扩展 **会被** 报告(针对范围模式的模式错误):这些确实是规范偏差。 diff --git a/docs/zh/index.md b/docs/zh/index.md new file mode 100644 index 0000000..1a651c7 --- /dev/null +++ b/docs/zh/index.md @@ -0,0 +1,32 @@ +# sil-lift + +一个用于 [LIFT](https://github.com/sillsdev/lift-standard) 的 Python 库 (词典交换格式) 0.13:支持 LIFT 文件夹(`.lift` + `.lift-ranges` + 媒体引用)的无损读写、模式和语义验证以及规范排序——并为大型词典提供了流式 API。 + +**状态:预发布版,正在积极开发中。** + +## 安装 + +摘自 [PyPI](https://pypi.org/project/sil-lift/): + +``` +pip install sil-lift # 安装库及 sil-lift 命令 +``` + +需要 Python 3.11 及以上版本。 唯一的运行时依赖项是 lxml。 + +## 30秒导览 + +```python +import sil_lift + +lex = sil_lift.load("thesaurus.lift") # 同时追踪 .lift-ranges 关联项 + +for entry in lex.entries: + if "en" not in entry.gloss_langs(): + print(entry.id, str(entry.lexical_unit.get("seh") or "")) + +entry = lex.find(guid="0f5a9c3e-...") # 或 lex.find(id="hoofd_a1b2") +entry.senses[0].definition["en"] = "head (anatomy)" + +lex.save() # 未修改的条目字节内容相同;已编辑的条目将重新序列化 +``` diff --git a/docs/zh/reference.md b/docs/zh/reference.md new file mode 100644 index 0000000..66ff4fb --- /dev/null +++ b/docs/zh/reference.md @@ -0,0 +1,9 @@ +# API 参考文档 + +公共 API 正是 `sil_lift` 在顶层重新导出的内容;以下划线为前缀的模块属于实现细节。 + +::: sil_lift +options: +show_root_heading: false +show_source: false +members_order: source