-- allium: 1
-- bDS Internationalization
-- Scope: core (Wave 0 onward — split localization is mandatory)
-- Distilled from: src/main/shared/i18n.ts, i18n/locales/*.json

value SupportedLanguage {
    code: String
    -- en, de, fr, it, es
    flag: String
    -- en=GB, de=DE, fr=FR, it=IT, es=ES
}

config {
    supported_languages: Set<SupportedLanguage> = {
        SupportedLanguage(code: "en", flag: "GB"),
        SupportedLanguage(code: "de", flag: "DE"),
        SupportedLanguage(code: "fr", flag: "FR"),
        SupportedLanguage(code: "it", flag: "IT"),
        SupportedLanguage(code: "es", flag: "ES")
    }
    default_language: String = "en"
}

-- ─── Split localization ─────────────────────────────────────
--
-- Two independent locale scopes exist per running app. They are
-- resolved by separate rules from separate inputs and may differ.

entity LocaleScopes {
    -- Singleton holding the two locale scopes.
    ui_locale: String
        -- Follows the OS system locale; used for menus and app chrome
    render_locale: String
        -- Follows the active project's mainLanguage; used for
        -- template rendering and generated content
}

surface LocaleSurface {
    facing _: LocaleConsumer

    provides:
        OsLocaleDetected(os_locale)
        ProjectMainLanguageResolved(main_language)
        MenuTranslationRequested(item_key)
        RenderTranslationRequested(label_key)
}

-- ─── Language normalization ─────────────────────────────────
--
-- Both scopes normalize their input the same way: take the base
-- language code, fall back to the default when unsupported.

rule NormalizeUiLocale {
    when: OsLocaleDetected(os_locale)
    let base = base_language_code(os_locale)
        -- black box: substring of the input before the first "-"
        -- ("en-US" -> "en")
    let codes = config.supported_languages -> code
    ensures:
        if base in codes:
            LocaleScopes.ui_locale = base
        else:
            LocaleScopes.ui_locale = config.default_language
}

rule NormalizeRenderLocale {
    when: ProjectMainLanguageResolved(main_language)
    let base = base_language_code(main_language)
    let codes = config.supported_languages -> code
    ensures:
        if base in codes:
            LocaleScopes.render_locale = base
        else:
            LocaleScopes.render_locale = config.default_language
}

-- ─── Translation lookup scopes ──────────────────────────────

rule TranslateMenuLabel {
    when: MenuTranslationRequested(item_key)
    -- Menus follow the OS convention: labels match the system
    -- language (UI locale), never the render locale.
    ensures: MenuLabelTranslated(item_key, locale: LocaleScopes.ui_locale)
}

rule TranslateRenderLabel {
    when: RenderTranslationRequested(label_key)
    -- Template rendering i18n strings (date formats, archive labels,
    -- "older posts", "newer posts", etc.) come from the locale JSON
    -- files for the render locale.
    ensures: RenderLabelTranslated(label_key, locale: LocaleScopes.render_locale)
}

-- ─── Search stemmer coverage ────────────────────────────────

invariant SnowballStemmerCoverage {
    -- ISO 639-1 codes with a dedicated Snowball algorithm
    -- (stemmer_for_language in bds-core fts.rs); nb/nn/no share the
    -- Norwegian stemmer, unknown codes fall back to English.
    -- Every UI language must have real stemming, not the fallback.
    let stemmed_codes = {"ar", "da", "de", "el", "en", "es", "fi", "fr",
        "hu", "it", "nb", "nn", "no", "nl", "pt", "ro", "ru", "sv", "ta", "tr"}
    for lang in config.supported_languages:
        lang.code in stemmed_codes
}
