From ae1c99421a9ec83239f01a6465004cf87c124c92 Mon Sep 17 00:00:00 2001 From: Morion Date: Wed, 11 Nov 2026 22:50:21 -0500 Subject: [PATCH] Add MaterialCraft catalog migrated from its Rust translation table --- ATTRIBUTION.md | 13 ++ docs/materialcraft-migration.md | 26 +++ locales/ar/materialcraft.tsv | 364 +++++++++++++++++++++++++++++ tools/export_materialcraft.py | 36 +++ tools/export_texturecraft.py | 32 +-- tools/test_export_materialcraft.py | 91 ++++++++ 6 files changed, 547 insertions(+), 15 deletions(-) create mode 100644 docs/materialcraft-migration.md create mode 100644 locales/ar/materialcraft.tsv create mode 100644 tools/export_materialcraft.py create mode 100644 tools/test_export_materialcraft.py diff --git a/ATTRIBUTION.md b/ATTRIBUTION.md index f62840aa..543f458c 100644 --- a/ATTRIBUTION.md +++ b/ATTRIBUTION.md @@ -310,3 +310,16 @@ Synced from `photocraft@7c64653` to `photocraft@2772ac1` with ## TextureCraft initial migration The `texturecraft.tsv` catalogs for ar, de, es, fr, it, ja, ko, pt-br and zh-hans were migrated from [TextureCraft `21856d778347e13857f65c8f1107170797bd070f`](https://github.com/storytold/texturecraft/tree/21856d778347e13857f65c8f1107170797bd070f/crates/ui-egui/src/i18n), MIT OR Apache-2.0. Original wording and contributor credits remain in the upstream history. Migration tooling was AI-assisted; no new translations were authored and no additional native-speaker review was performed. Every original row was checked against the resolved app and shared catalogs. Tracked in storytold/texturecraft#419. + + +## MaterialCraft initial migration + +`locales/ar/materialcraft.tsv` was migrated from the Arabic Rust table in +[MaterialCraft `b73facdc7b8b5ce102cd9e14d964058a15182dc0`](https://github.com/storytold/materialcraft/blob/b73facdc7b8b5ce102cd9e14d964058a15182dc0/crates/ui-egui/src/i18n/ar.rs), +MIT OR Apache-2.0. Original wording and contributor credits remain in the upstream +history. Migration tooling was AI-assisted; no new translations were authored and +no additional native-speaker review was performed. All 400 original rows were +checked against the resolved app and shared catalogs; identical shared rows were +omitted and differing app translations retained as overrides. The unmerged Hebrew +catalog/font PRs storytold/materialcraft#197 and #198 are outside this snapshot. +Tracked in storytold/materialcraft#210. diff --git a/docs/materialcraft-migration.md b/docs/materialcraft-migration.md new file mode 100644 index 00000000..c3dd3dd0 --- /dev/null +++ b/docs/materialcraft-migration.md @@ -0,0 +1,26 @@ +# MaterialCraft initial catalog migration + +Source: `materialcraft@b73facdc7b8b5ce102cd9e14d964058a15182dc0`. +This pre-cutover main revision offers English (source text) and Arabic. The Hebrew +catalog/font work in MaterialCraft #197/#198 was not merged at export time. + +From the root of this repository, with Git, Python 3 and rustc available: + +```sh +python tools/export_materialcraft.py ../materialcraft --ref b73facdc7b8b5ce102cd9e14d964058a15182dc0 +python tools/export_materialcraft.py ../materialcraft --ref b73facdc7b8b5ce102cd9e14d964058a15182dc0 --check +python -m unittest discover -s tools +cargo test --workspace +``` + +The export compiles the original Rust table so escaped and raw strings are decoded +by Rust itself. Existing shared rows are reused only when the translation matches +exactly; differing wording stays in materialcraft.tsv. `--check` writes nothing and +compares every original translation with the resolved app/shared catalogs. +English needs no catalog, and no missing translations are invented. + +This is the catalog prerequisite for storytold/materialcraft#210. MaterialCraft's +runtime cutover must pin this PR's actual upstream merge commit after acceptance, +record the translation source in its own ATTRIBUTION.md, and rerun parity against +current main. Include any locale work merged meanwhile before removing Rust tables. +The catalog PR does not close the application issue or change MaterialCraft code. diff --git a/locales/ar/materialcraft.tsv b/locales/ar/materialcraft.tsv new file mode 100644 index 00000000..dc65fe2b --- /dev/null +++ b/locales/ar/materialcraft.tsv @@ -0,0 +1,364 @@ +# العربية (ar) interface catalog for MaterialCraft. +# Columns, separated by a TAB: context English source text translation +# context usually empty. `@plural`, `@id` and `@msg` mean something to the apps; any other +# word is a label that tells apart two meanings of the same English. +# escapes \n \t \\ inside a column. `{name}` placeholders must appear in both columns (in any +# order), and a trailing "…" in one needs one in the other. +# @plural English `one|other`; the translation lists this language's plural forms, `|`-separated. +# Lines starting with # and blank lines are ignored. A missing entry shows in English. +# +# Clean room: every entry was written from the plain meaning of the English text in ordinary +# computing vocabulary. No other product's localisation, string table or translated UI was +# consulted or copied; add entries the same way. + + $pos $pos + $size $size + (checker) (رقعة شطرنج) + .mcmat export isn't available yet (#190) تصدير .mcmat غير متاح بعد (#190) + 1024 px 1024 بكسل + 16 bits 16 بت + 16 bits per channel 16 بت لكل قناة + 16 bits per channel float (HDR) 16 بت لكل قناة فاصلة عائمة (HDR) + 2048 px 2048 بكسل + 256 px 256 بكسل + 2D View عرض ثنائي الأبعاد + 32 bits float 32 بت فاصلة عائمة + 32 bits per channel float (HDR) 32 بت لكل قناة فاصلة عائمة (HDR) + 3D pan limit (mesh radii) حد التحريك ثلاثي الأبعاد (أنصاف أقطار المجسم) + 3D pan speed سرعة التحريك ثلاثي الأبعاد + 3D View عرض ثلاثي الأبعاد + 512 px 512 بكسل + 8 bits 8 بت + 8 bits per channel 8 بت لكل قناة + A folder: material.mtlx, material.json, resources/ and the optional preview.png مجلد: material.mtlx وmaterial.json وresources/ وpreview.png الاختياري + About MaterialCraft حول MaterialCraft + About MaterialCraft... حول MaterialCraft... + Accent color لون التمييز + Actual pixels البكسلات الفعلية + Add Comment إضافة تعليق + Add comment إضافة تعليق + Add Dot node إضافة عقدة نقطية + Add Frame إضافة إطار + Add frame around selection إضافة إطار حول التحديد + Add frame around the selection إضافة إطار حول التحديد + Add Navigation pin إضافة دبوس تنقل + Add navigation pin إضافة دبوس تنقل + Add node إضافة عقدة + Add Output nodes with usages (base color, normal, ...) to see the material أضف عُقد مخرجات مع استخداماتها (اللون الأساسي، الناظم، ...) لمعاينة المادة + Align centers horizontally محاذاة المراكز أفقياً + Align centers vertically محاذاة المراكز رأسياً + Align top محاذاة للأعلى + All nodes كل العقد + Amber كهرماني + An open-source Crafting App by the ArtCraft team and community. تطبيق تصميم مفتوح المصدر من قِبل فريق ومجتمع ArtCraft. + And و (And) + Arc tangent 2 ظل زاوية قوسي 2 + ArtCraft website... موقع ArtCraft الإلكتروني... + As JSON بصيغة JSON + Atomic nodes عقد أولية + Attributes السمات + Automatic (16 bits for normal and height) تلقائي (16 بت للناظم والارتفاع) + Automatic export when outputs change تصدير تلقائي عند تغيّر المخرجات + Autosave every (minutes, 0 = off) الحفظ التلقائي كل (دقائق، 0 = معطل) + Bad function دالة غير صالحة + Bake every control خَبز كل عناصر التحكم في الخامات + Base parameters المعلمات الأساسية + Batch دفعة + Bind to input ربط بالمدخل + Bit depth عمق البت + Blends عمليات الدمج + Boolean منطقي + Brick wall جدار طوب + Brightness السطوع + Build tileable materials from noises, patterns and filters, see them live in 2D and 3D, and export the maps for any engine. أنشئ مواد قابلة للتكرار من التشويش والأنماط والمرشحات، وعاينها مباشرة ثنائية وثلاثية الأبعاد، وصدّر الخرائط لأي محرك. + Cache الذاكرة المؤقتة + Camera الكاميرا + Cast تحويل النوع + Ceil الحد الأعلى (Ceil) + Check at least one output to export حدّد مخرجًا واحدًا على الأقل للتصدير + Check for updates... التحقق من وجود تحديثات... + Choose a folder to export to اختر مجلداً للتصدير إليه + Choose... اختيار... + Clamp تقييد النطاق (Clamp) + Clear cache مسح الذاكرة المؤقتة + Clear recent packages مسح الحزم الأخيرة + Close all إغلاق الكل + color space فضاء الألوان + Community (Discord) المجتمع (Discord) + Comparison مقارنة + Compute graph حساب المخطط + Computing... جاري الحساب... + Console وحدة التحكم + Constant ثابت + Control تحكم + Copper نحاسي + Copy selection نسخ التحديد + Coral مرجاني + Cosine جيب التمام (Cosine) + Couldn't export تعذر التصدير + Couldn't import تعذر الاستيراد + Couldn't load تعذر التحميل + Couldn't open تعذر الفتح + Couldn't save تعذر الحفظ + Create graph from selection إنشاء مخطط من التحديد + Create graph in إنشاء مخطط في + Cube مكعب + Cylinder أسطوانة + Delete and relink حذف وإعادة ربط + Delete input حذف المدخل + Delete preset حذف الإعداد المسبق + Delete selection حذف التحديد + Destination الوجهة + DirectX (Y-) DirectX (Y-) + Disable selection تعطيل التحديد + Displacement scale مقياس الإزاحة + Distribute horizontally توزيع أفقي + Distribute vertically توزيع رأسي + Division قسمة + Documentation التوثيق + Documentation... التوثيق... + Dot Product الجداء السلمي (Dot Product) + Double-click a node to show it here انقر نقراً مزدوجاً على عقدة لعرضها هنا + Downloaded تم التنزيل + Duplicate selection تكرار التحديد + Duplicate selection without link تكرار التحديد بدون ربط + Edit function graph... تحرير مخطط الدوال... + Empty فارغ + Empty package حزمة فارغة + Engine المحرك + Environment البيئة + Environment background خلفية البيئة + Equal مساوٍ + Exit خروج + Explorer المستكشف + Exponential دالة أسية (Exp) + Export material... تصدير المادة... + Export outputs تصدير المخرجات + Export outputs... تصدير المخرجات... + Export outputs... (Ctrl+Shift+E) تصدير المخرجات... (Ctrl+Shift+E) + Export settings saved حُفظت إعدادات التصدير + Exported تم التصدير + Exported color space فضاء الألوان المُصدَّر + Expose as graph input كشف كمدخل للمخطط + Exposed parameters المعلمات المكشوفة + Exposure التعريض + File names أسماء الملفات + files ملفات + Filters مرشحات + Float فاصلة عائمة (Float) + Float2 فاصلة عائمة ثنائية (Float2) + Float3 فاصلة عائمة ثلاثية (Float3) + Float4 فاصلة عائمة رباعية (Float4) + Flood fill تعبئة متصلة (Flood fill) + Floor الحد الأدنى (Floor) + Folder المجلد + Fractional part الجزء الكسري + Frame all تأطير الكل + Frame all (F) تأطير الكل (F) + Frame all / the selection تأطير الكل / التحديد + From graph من المخطط + Front أمامي + Full size الحجم الكامل + full size الحجم الكامل + Function دالة + function دالة + Function graph مخطط الدوال + function nodes عقد الدوال + Function of دالة لـ + Get variable جلب متغير + Go to pin الانتقال إلى الدبوس + Graph مخطط + Graph instance نسخة المخطط + Graph items عناصر المخطط + Graph name اسم المخطط + Graph's size حجم المخطط + Graph... مخطط... + Graphs in this package المخططات في هذه الحزمة + Greater أكبر من + Greater or equal أكبر من أو يساوي + Height (parallax) depth عمق الارتفاع (اختلاف المنظر) + Histogram المخطط البياني (Histogram) + History السجل + Home screen... الشاشة الرئيسية... + Icons أيقونات + If...Else إذا...وإلا (If...Else) + Import bitmap... استيراد صورة نقطية... + Imported تم الاستيراد + Include preview.png تضمين preview.png + Input parameters معلمات المدخلات + Input previews معاينات المدخلات + Instance parameters معلمات النسخة + Integer عدد صحيح + Interface scale حجم واجهة المستخدم + Join the community (Discord)... انضم إلى المجتمع (Discord)... + Keep supported controls إبقاء عناصر التحكم المدعومة + Key at مفتاح عند + Last compute آخر حساب + Lava rock صخور بركانية + Library المكتبة + Linear interpolation استيفاء خطي (Lerp) + Link width and height ربط العرض والارتفاع + Load mesh... تحميل مجسم... + Logarithm لوغاريتم + Logical منطقي + Lower أصغر من + Lower or equal أصغر من أو يساوي + Mat ball كرة المادة + Material filters مرشحات المواد + MaterialX export isn't available yet (#190) تصدير MaterialX غير متاح بعد (#190) + Maximum الحد الأقصى + Medium dark داكن متوسط + Medium light فاتح متوسط + Minimum الحد الأدنى + Modulo باقي القسمة (Modulo) + Mossy ground أرضية مغطاة بالطحالب + Multiplication ضرب + Negation نفي / سالب + New graph مخطط جديد + New graph... مخطط جديد... + New graph... (Ctrl+N) مخطط جديد... (Ctrl+N) + New random seed بذرة عشوائية جديدة + Night ليل + No font on this system can show this language لا يوجد خط على هذا النظام يمكنه عرض هذه اللغة + No graph is open لا يوجد مخطط مفتوح + No graph with outputs لا يوجد مخطط له مخرجات + No matching node لا توجد عقدة مطابقة + No presets yet لا توجد إعدادات مسبقة بعد + No recent packages لا توجد حزم حديثة + Node search (over the graph) البحث عن عقدة (فوق المخطط) + Node-based procedural materials, built in pure Rust. مواد إجرائية قائمة على العقد، مبنية بلغة Rust النقية. + Noises تشويشات + None بلا + Normal الناظم (Normal) + Normal orientation اتجاه الناظم (Normal) + Normalize تسوية (Normalize) + Not ليس (Not) + not available yet غير متاح بعد + Not equal غير مساوٍ + Nothing matches لا توجد تطابقات + One .mcmat archive: material.mtlx, material.json, resources/ and the optional preview.png أرشيف .mcmat واحد: material.mtlx وmaterial.json وresources/ وpreview.png الاختياري + Open graph فتح المخطط + Open package... فتح حزمة... + Open package... (Ctrl+O) فتح حزمة... (Ctrl+O) + Open... فتح... + Opened تم الفتح + OpenGL (Y+) OpenGL (Y+) + Operator معامل حسابي + Ops عمليات + Or أو (Or) + Output format صيغة المخرج + Output size حجم المخرج + Output's format صيغة المخرج + Outputs المخرجات + Overcast غائم + Painted metal plates صفائح معدنية مطلية + Pan the camera تحريك الكاميرا + Pan the graph تحريك المخطط + Parent size: حجم العنصر الأصل: + Patterns أنماط + PBR material (outputs only) مادة PBR (مخرجات فقط) + Perlin noise تشويش بيرلين (Perlin noise) + Pick an image file (a mesh map baked in TextureCraft) اختر ملف صورة (خريطة مجسم مخبوزة في TextureCraft) + Plane مستوى + Polished marble رخام مصقول + Power قوة / أس (Power) + Preferences... التفضيلات... + Preset name اسم الإعداد المسبق + Presets الإعدادات المسبقة + Preview size حجم المعاينة + Preview size while editing حجم المعاينة أثناء التحرير + Properties الخصائص + Publishing arrives with the new .mcmat format (#190); these options are kept for it يصل النشر مع صيغة .mcmat الجديدة (#190)؛ وتُحفظ هذه الخيارات لها + Random عشوائي + Random seed بذرة عشوائية + Recent packages الحزم الأخيرة + Redo (Ctrl+Y) إعادة (Ctrl+Y) + Relative to input نسبي إلى المدخل + Relative to parent نسبي إلى الأصل + Reload resources إعادة تحميل الموارد + Remove function إزالة الدالة + Render time وقت التصيير + Renderer المصير + Report a bug... الإبلاغ عن خطأ... + Reset layout إعادة ضبط التخطيط + Reset parameters إعادة ضبط المعلمات + Reset scene إعادة ضبط المشهد + Reset to default إعادة ضبط للافتراضي + Resources الموارد + Right-click a node parameter > Expose as graph input انقر بزر الفأرة الأيمن على معلمة العقدة > كشف كمدخل للمخطط + Right-click or Space to add operations; drag from an output dot to an input; the starred node is the result. انقر بزر الفأرة الأيمن أو مفتاح المسافة لإضافة عمليات؛ اسحب من نقطة المخرج إلى المدخل؛ العقدة المميزة بنجمة هي النتيجة. + Rounded cube مكعب مدور الحواف + Rusty metal معدن صدئ + Sample Color عينة لونية + Sample Gray عينة تدرج رمادي + Samplers عينات + Samples عينات + Save (Ctrl+S) حفظ (Ctrl+S) + Save as... حفظ باسم... + Save image... حفظ الصورة... + Save package حفظ الحزمة + Save preset حفظ الإعداد المسبق + Save settings حفظ الإعدادات + Saved تم الحفظ + Scene المشهد + Search nodes... البحث في العقد... + Select all تحديد الكل + Select downstream nodes تحديد العقد اللاحقة + Select upstream nodes تحديد العقد السابقة + Send to TextureCraft إرسال إلى TextureCraft + Sent to TextureCraft: find it on the shelf under My smart materials أُرسل إلى TextureCraft: ستجده على الرف ضمن موادي الذكية + Set function... تعيين دالة... + Shortcuts الاختصارات + Shortcuts list... قائمة الاختصارات... + Show at startup إظهار عند بدء التشغيل + Show in the 2D view إظهار في العرض ثنائي الأبعاد + Show node thumbnails إظهار الصور المصغرة للعقد + Showing عرض + Side جانبي + Sine جيب الزاوية (Sine) + Specific parameters المعلمات المخصصة + Sphere كرة + Square root جذر تربيعي + Start export بدء التصدير + Stone tiles بلاط حجري + Studio استوديو + Subtraction طرح + Sunset غروب + SVG رسوم متجهة (SVG) + Swizzle إعادة ترتيب القنوات (Swizzle) + Tangent ظل الزاوية (Tangent) + Templates القوالب + Texture generators مولدات الخامات + Textures الخامات + The instanced graph is missing المخطط المنسوخ مفقود + This graph exposes no inputs هذا المخطط لا يكشف أي مدخلات + This graph has no outputs هذا المخطط ليس له مخرجات + This package هذه الحزمة + threads خيوط معالجة + Tile 3x3 تكرار 3x3 + Tiling التكرار + Tiling (H and V) التكرار (أفقياً ورأسياً) + To Float إلى فاصلة عائمة + To Integer إلى عدد صحيح + Tools أدوات + Top علوي + Torus شكل حلقي (Torus) + Transforms التحويلات + Turn the environment تدوير البيئة + Unbind فك الارتباط + Undo (Ctrl+Z) تراجع (Ctrl+Z) + Use as result استخدام كنتيجة + Vector متجه + Vector Float2 متجه ثنائي (Float2) + Vector Float3 متجه ثلاثي (Float3) + Vector Float4 متجه رباعي (Float4) + View in 2D view عرض في العرض ثنائي الأبعاد + Violet بنفسجي + Weathering التجوية والتقادم + Welcome to MaterialCraft مرحباً بك في MaterialCraft + Wicker weave نسيج قش + Windows نوافذ + Wood planks ألواح خشبية + Woven fabric قماش منسوج + Zoom the graph and the views تقريب المخطط وطرق العرض + {format} export isn't available yet تصدير {format} غير متاح بعد diff --git a/tools/export_materialcraft.py b/tools/export_materialcraft.py new file mode 100644 index 00000000..88d3918c --- /dev/null +++ b/tools/export_materialcraft.py @@ -0,0 +1,36 @@ +#!/usr/bin/env python3 +"""Migrate MaterialCraft's Rust translation tables (storytold/materialcraft#210). + +python tools/export_materialcraft.py ../materialcraft --ref +python tools/export_materialcraft.py ../materialcraft --ref --check + +Requires Git and rustc. Use a pre-cutover source commit and record the full SHA in +ATTRIBUTION.md. This initial export preserves existing translations; it does not +create translations or import unmerged locale work. +""" +import argparse +from pathlib import Path + +from export_texturecraft import migrate as migrate_tables + +ROOT = Path(__file__).resolve().parent.parent +LANGUAGES = {'ar': 'العربية'} + + +def migrate(checkout, revision, root, check=False): + """Export Arabic to materialcraft.tsv or verify resolved translations without writes.""" + migrate_tables(checkout, revision, root, check, languages=LANGUAGES, app='materialcraft', name='MaterialCraft') + + +def main(): + """Read the source checkout, fixed revision and optional read-only check mode.""" + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('checkout', type=Path) + parser.add_argument('--ref', required=True) + parser.add_argument('--check', action='store_true') + args = parser.parse_args() + migrate(args.checkout.resolve(), args.ref, ROOT, args.check) + + +if __name__ == '__main__': + main() diff --git a/tools/export_texturecraft.py b/tools/export_texturecraft.py index 5ad3c06a..8f9bd83b 100644 --- a/tools/export_texturecraft.py +++ b/tools/export_texturecraft.py @@ -29,17 +29,18 @@ def output(*args, cwd=None): return subprocess.check_output(args, cwd=cwd, text=True, encoding='utf-8') -def original_tables(checkout, revision): - """Compile the nine tables at a fixed Git revision and return escaped TSV rows.""" +def original_tables(checkout, revision, languages=None): + """Compile the configured tables at a fixed Git revision and return escaped TSV rows.""" + languages = LANGUAGES if languages is None else languages with tempfile.TemporaryDirectory() as directory: temp = Path(directory) code = [r'fn esc(s: &str) -> String { s.replace("\\", "\\\\").replace("\t", "\\t").replace("\n", "\\n") }'] - for module in LANGUAGES: + for module in languages: source = output('git', 'show', f'{revision}:crates/ui-egui/src/i18n/{module}.rs', cwd=checkout) (temp / f'{module}.rs').write_text(source, encoding='utf-8') code.append(f'mod {module};') code.append('fn main() {') - for module in LANGUAGES: + for module in languages: code.append(f'for (source, value) in {module}::TABLE {{ println!("{module}\\t{{}}\\t{{}}", esc(source), esc(value)); }}') code.append('}') source = temp / 'export.rs' @@ -68,40 +69,41 @@ def read(path): return rows -def resolved(root, locale): +def resolved(root, locale, app="texturecraft"): """App overrides shared rows; a regional locale can inherit its parent.""" rows = {} parts = locale.split('-') if len(parts) > 1 and (len(parts[-1]) == 2 or parts[-1].isdigit()): - rows.update(resolved(root, '-'.join(parts[:-1]))) + rows.update(resolved(root, '-'.join(parts[:-1]), app)) rows.update(read(root / 'locales' / locale / 'common.tsv')) - rows.update(read(root / 'locales' / locale / 'texturecraft.tsv')) + rows.update(read(root / 'locales' / locale / f'{app}.tsv')) return rows -def migrate(checkout, revision, root, check=False): +def migrate(checkout, revision, root, check=False, *, languages=None, app="texturecraft", name="TextureCraft"): """Export an initial catalog snapshot or verify it without writes when check is true.""" + languages = LANGUAGES if languages is None else languages sha = output('git', 'rev-parse', '--verify', revision + '^{commit}', cwd=checkout).strip() - tables = original_tables(checkout, sha) + tables = original_tables(checkout, sha, languages) for locale, original in tables.items(): - path = root / 'locales' / locale / 'texturecraft.tsv' + path = root / 'locales' / locale / f'{app}.tsv' if not check: # Exclude only rows already supplied by shared/parent catalogs. A regional # app row differing from its own shared catalog must remain an override. inherited = {} parts = locale.split('-') if len(parts) > 1 and (len(parts[-1]) == 2 or parts[-1].isdigit()): - inherited.update(resolved(root, '-'.join(parts[:-1]))) + inherited.update(resolved(root, '-'.join(parts[:-1]), app)) inherited.update(read(path.with_name('common.tsv'))) own = {key: value for key, value in original.items() if inherited.get(key) != value} - name = LANGUAGES[locale.replace('-', '_')] - path.write_text(render(name, locale, 'TextureCraft', own), encoding='utf-8', newline='\n') - actual = resolved(root, locale) + language_name = languages[locale.replace('-', '_')] + path.write_text(render(language_name, locale, name, own), encoding='utf-8', newline='\n') + actual = resolved(root, locale, app) differences = [source for key, value in original.items() if actual.get(key) != value for source in [key[1]]] if differences: raise ValueError(f'{locale}: translations changed: {differences}') print(f'{locale}: {len(original)} original rows resolve identically') - print(f'Source: texturecraft@{sha}') + print(f'Source: {app}@{sha}') def main(): diff --git a/tools/test_export_materialcraft.py b/tools/test_export_materialcraft.py new file mode 100644 index 00000000..92fd84b4 --- /dev/null +++ b/tools/test_export_materialcraft.py @@ -0,0 +1,91 @@ +"""Regression tests for the MaterialCraft initial catalog migration.""" +import contextlib +import io +import tempfile +import unittest +from pathlib import Path +from unittest.mock import patch + +import export_materialcraft as material +import export_texturecraft as shared + + +class MaterialCraftExportTests(unittest.TestCase): + """Keep app isolation, shared overrides and read-only parity checking explicit.""" + + def migrate(self, root, rows, check=False): + """Supply a fixed source snapshot while exercising the real TSV migration.""" + with patch.object(shared, 'original_tables', return_value={'ar': rows}) as tables: + with patch.object(shared, 'output', return_value='a' * 40), contextlib.redirect_stdout(io.StringIO()): + material.migrate(Path('checkout'), 'fixed', root, check) + tables.assert_called_once_with(Path('checkout'), 'a' * 40, material.LANGUAGES) + + def fixture(self, root): + """Create separate shared and TextureCraft rows that must not be overwritten.""" + locale = root / 'locales/ar' + locale.mkdir(parents=True) + (locale/'common.tsv').write_text('\tSave\tShared save\n\tOpen\tShared open\n', encoding='utf-8') + (locale/'texturecraft.tsv').write_text('\tLayers\tTexture only\n', encoding='utf-8') + return locale + + def test_export_preserves_shared_differences_without_changing_other_apps(self): + """Keep a differing shared translation as an app override; omit identical rows.""" + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + locale = self.fixture(root) + before = {p.name: p.read_bytes() for p in locale.iterdir()} + rows = {('', 'Save'): 'Shared save', ('', 'Open'): 'Material open', ('', 'Nodes'): 'عقد'} + self.migrate(root, rows) + own = shared.read(locale/'materialcraft.tsv') + self.assertNotIn(('', 'Save'), own) + self.assertEqual(own[('', 'Open')], 'Material open') + self.assertEqual(shared.resolved(root, 'ar', 'materialcraft')[('', 'Nodes')], 'عقد') + for name, content in before.items(): + self.assertEqual((locale/name).read_bytes(), content) + self.assertIn('MaterialCraft', (locale/'materialcraft.tsv').read_text(encoding='utf-8')) + + def test_check_mode_does_not_write_or_reformat_files(self): + """A parity check preserves file bytes and modification times exactly.""" + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + locale = self.fixture(root) + rows = {('', 'Open'): 'Material open'} + self.migrate(root, rows) + before = {p: (p.read_bytes(), p.stat().st_mtime_ns) for p in locale.iterdir()} + self.migrate(root, rows, check=True) + self.assertEqual(before, {p: (p.read_bytes(), p.stat().st_mtime_ns) for p in locale.iterdir()}) + + def test_check_rejects_changed_translation(self): + """Detect a value changed after export rather than silently accepting it.""" + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + locale = self.fixture(root) + (locale/'materialcraft.tsv').write_text('\tNodes\tChanged\n', encoding='utf-8') + with self.assertRaisesRegex(ValueError, 'translations changed'): + self.migrate(root, {('', 'Nodes'): 'عقد'}, check=True) + + def test_check_rejects_missing_translation_without_creating_catalog(self): + """Missing MaterialCraft rows cannot be satisfied by another app's table.""" + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + locale = self.fixture(root) + with self.assertRaisesRegex(ValueError, 'translations changed'): + self.migrate(root, {('', 'Layers'): 'Texture only'}, check=True) + self.assertFalse((locale/'materialcraft.tsv').exists()) + + def test_actual_rust_extraction_reads_only_the_configured_locale(self): + """Use rustc to decode the single Arabic table and its escaped Rust literals.""" + source = r'''pub const TABLE: &[(&str, &str)] = &[("Line\nTab\tSlash\\", "عقد")];''' + real_output = shared.output + + def output(*args, cwd=None): + """Replace Git's source with a fixture but keep the real compiler execution.""" + return source if args[0] == 'git' else real_output(*args, cwd=cwd) + + with patch.object(shared, 'output', side_effect=output): + tables = shared.original_tables(Path('.'), 'fixed', material.LANGUAGES) + self.assertEqual(tables, {'ar': {('', r'Line\nTab\tSlash\\'): 'عقد'}}) + + +if __name__ == '__main__': + unittest.main()