-- allium: 1 -- bDS Translation System -- Scope: core (Wave 1) -- Distilled from: src/main/engine/PostEngine.ts (translation methods), -- postTranslationFileUtils.ts, MediaEngine.ts use "./post.allium" as post use "./media.allium" as media enum PostTranslationStatus { draft published } surface TranslationControlSurface { facing _: TranslationOperator provides: UpsertPostTranslationRequested(post, language, title, content, excerpt) DeletePostTranslationRequested(translation) ValidateTranslationsRequested(project) } surface TranslationRuntimeSurface { facing _: TranslationRuntime provides: PostPublished(post) PublishTranslationRequested(translation) TranslationEdited(translation, changes) } value SupportedLanguage { -- en, de, fr, it, es code: String } surface SupportedLanguageSurface { context language: SupportedLanguage exposes: language.code } entity PostTranslation { canonical_post: post/Post language: SupportedLanguage title: String excerpt: String? content: String? status: PostTranslationStatus file_path: String checksum: String? created_at: Timestamp updated_at: Timestamp published_at: Timestamp? -- Derived content_location: if status = published: file_path else: content transitions status { draft -> published published -> draft } } invariant TranslationFilesStoreOnlyLanguageSpecificMetadata { -- Translation markdown files persist only fields that differ by language. -- Shared metadata such as publication status and timestamps belongs to the -- canonical post file and is inherited from the canonical post when -- rebuilding or diffing translation files. for t in PostTranslations where file_path != "": translation_file(t).omits_shared_metadata = true } surface PostTranslationSurface { context translation: PostTranslation exposes: translation.canonical_post translation.language.code translation.title translation.excerpt when translation.excerpt != null translation.content when translation.content != null translation.status translation.file_path translation.checksum when translation.checksum != null translation.created_at translation.updated_at translation.published_at when translation.published_at != null translation.content_location } invariant UniqueTranslationPerLanguage { for a in PostTranslations: for b in PostTranslations: (a != b and a.canonical_post = b.canonical_post) implies a.language != b.language } invariant TranslationFilePath { -- posts/YYYY/MM/{slug}.{language}.md for t in PostTranslations where file_path != "": t.file_path = format("posts/{yyyy}/{mm}/{slug}.{lang}.md", yyyy: t.canonical_post.created_at.year, mm: t.canonical_post.created_at.month_padded, slug: t.canonical_post.slug, lang: t.language.code) } rule UpsertPostTranslation { when: UpsertPostTranslationRequested(post, language, title, content, excerpt) requires: not post.do_not_translate ensures: let translation = PostTranslation.created( canonical_post: post, language: language, title: title, content: content, excerpt: excerpt, status: draft, file_path: "" ) translation.status = draft -- If translation already exists, update it instead } rule PublishPostTranslation { when: PostPublished(post) -- All translations are also published when the canonical post is published for t in post.translations: ensures: PublishTranslationRequested(t) } rule PublishTranslation { when: PublishTranslationRequested(translation) requires: translation.status = draft ensures: translation.status = published ensures: translation.published_at = translation.published_at ?? now ensures: TranslationFileWritten(translation) ensures: translation.content = null -- Content moves to filesystem } rule ReopenPublishedTranslation { when: TranslationEdited(translation, changes) requires: translation.status = published requires: translation_edit_affects_published_content(changes) ensures: translation.status = draft ensures: translation.updated_at = now } rule DeletePostTranslation { when: DeletePostTranslationRequested(translation) ensures: not exists translation ensures: TranslationFileDeleted(translation) ensures: SearchIndexUpdated(translation.canonical_post) -- FTS index includes all translations of a post } rule ValidateTranslations { when: ValidateTranslationsRequested(project) -- Checks all posts against configured blog languages -- Reports: missing translations, orphan translation files, -- posts marked do_not_translate for post in project.posts where status = published: for lang in project.blog_languages: if lang != project.main_language: if not post.do_not_translate: if not (lang in post.available_languages): ensures: ValidationIssueReported(post, lang, "missing") @guidance -- This produces a validation report, not automatic fixes -- The report drives targeted re-rendering } invariant FtsIncludesTranslations { -- Full-text search index for a post includes stemmed content -- from all its translations, not just the canonical language for post in Posts: includes_text(search_index(post), post.title) for t in post.translations: includes_text(search_index(post), t.title) }