Localization of smoke signal

So I scattered around the code and templates and discovered that you already have some work done to help with localization. I have some questions :

  • Is Fluent used only for the rust backend error messages ? Are there strings in backend i should check ?

  • If i translate all files in templates/ and add them as .fr-ca.html, how can i switch ? I can see that you seems to pump from headers and/or a cookie.

Ok. so i dug up some more and you indeed put language selection ! My bad.

Have you thought of maybe separating templates in languages folders in the future ?

Hello @kayrozen! Yeah, the app has some of the primitives for i18n. Like you said, I’m using Fluent for backend generated strings (errors primarily) and then template files that have the language in their name.

I have and I think it’s a direction that is definitely worth investing in. Once I got through the 10th or so template, I realized that the current pattern won’t scale that well. I started with the i18n/ folder and would like to do the same to templates/`

I didn’t add it to the footer, but using a form POST to /language with the language attribute will lookup the given language against configured values and set a session cookie with it. Additionally, language can be set as a permanent user setting on the account settings page.

The language selection middleware will attempt to select a language in the following order:

  1. If the visitor has an active and valid auth session, the user’s language is used
  2. If the web session has a language cookie set
  3. If the browser is providing a language hint
  4. The default (first configured) language
1 Like

I propose to use fluent also for i18n of templates. That way, templating and i18n will be separate and easier to delegate.

As Claude tells me, it shouln’t be too complex. (I don’t know Rust but working on my skills.)

As i got access to Claude 4 via my github sub, i’ve asked it to propose me some options as to how implement i18n. Here’s what it suggested.

I know that it is a debate right now about AI “vibe” coding so if you prefer not rely on it, i’ll understand. But i think atproto in general is more open to it.

I also asked if there was specific things for latin root based langs and i have some things to add to the rust base to better reflect syntax and grammar. it is at the end if you’re curious.


My Recommendation: Option 1 with Enhancements

I recommend Option 1 with the following enhancements:

Enhanced Implementation Strategy:

  1. Create a comprehensive i18n template system:
// src/http/template_i18n.rs

pub struct TemplateI18n {
    locales: Locales,
    default_language: LanguageIdentifier,
}

impl TemplateI18n {
    pub fn setup_environment(&self, env: &mut Environment<'_>) {
        // Main translation filter
        let locales = self.locales.clone();
        env.add_filter("t", move |key: String, args: Value| -> Result<String, minijinja::Error> {
            let lang = args.get("lang")
                .and_then(|v| v.as_str())
                .and_then(|s| s.parse().ok())
                .unwrap_or_else(|| self.default_language.clone());
            
            let variables = self.extract_fluent_args(&args);
            locales.format_message(&lang, &key, variables.as_ref())
                .unwrap_or_else(|| format!("Missing: {}", key))
        });
        
        // Pluralization helper
        env.add_filter("tc", move |key: String, args: Value| -> Result<String, minijinja::Error> {
            // Handle count-based translations
            let count = args.get("count").and_then(|v| v.as_u64()).unwrap_or(1);
            // Implementation for pluralization
        });
    }
    
    fn extract_fluent_args(&self, args: &Value) -> Option<fluent::FluentArgs> {
        // Convert MiniJinja args to Fluent args
    }
}
  1. Enhance your WebContext integration:
// In src/http/context.rs
impl WebContext {
    pub fn create_template_context(&self, language: &LanguageIdentifier, additional: Value) -> Value {
        template_context! {
            ..additional,
            language => language.to_string(),
            // Add commonly used translations
            common => self.get_common_translations(language),
        }
    }
    
    fn get_common_translations(&self, language: &LanguageIdentifier) -> Value {
        // Pre-translate frequently used messages
        template_context! {
            loading => self.i18n_context.locales.format_message(language, "loading", None),
            error => self.i18n_context.locales.format_message(language, "error", None),
            // etc.
        }
    }
}

Template Usage Examples:

<h1>{{ "welcome-title" | t(lang=language) }}</h1>
<p>{{ "user-greeting" | t(lang=language, name=current_handle.display_name) }}</p>
<span>{{ "event-count" | tc(lang=language, count=events|length) }}</span>
<div class="loading">{{ common.loading }}</div>

Migration Strategy

  1. Phase 1: Implement the filter system alongside your existing template structure
  2. Phase 2: Gradually convert hardcoded strings in templates to Fluent messages
  3. Phase 3: Expand your Fluent message files beyond just errors
  4. Phase 4: Consider consolidating language-specific templates where appropriate

This approach leverages your existing solid i18n infrastructure while providing a clean, maintainable way to integrate Fluent with MiniJinja templates that scales well with your application’s complexity.

Potential Pitfalls with Latin-Based Languages for Fluent + MiniJinja

1. Gendered Language Issues :warning:

Problem: Romance languages like French have grammatical gender that affects articles, adjectives, and participles.

Example from your content:

Un point central où se croisent artistes, organisateurs d'événements et public

Pitfall: Simple key-value translation won’t handle cases where the gender of a variable affects surrounding words:

Problematic - doesn’t handle gender agreement

welcome-message = Bienvenu{ $userName } dans notre plateforme

Better approach:

Handle gender explicitly

welcome-message-masculine = Bienvenu { $userName } dans notre plateforme

welcome-message-feminine = Bienvenue { $userName } dans notre plateforme

welcome-message-neutral = Bienvenu·e { $userName } dans notre plateforme

Or use Fluent’s select expressions

welcome-message = { $userGender ->

[masculine] Bienvenu { $userName }

[feminine] Bienvenue { $userName }

*[other] Bienvenu·e { $userName }

} dans notre plateforme

2. Number Agreement Complexity :warning:

Problem: Romance languages have complex plural rules affecting multiple words in a sentence.

Pitfall: English-style pluralization doesn’t work:


# Too simplistic for French

event-count = { $count ->

[one] { $count } événement récent

*[other] { $count } événements récents

}

Better approach:

French requires agreement on multiple words

event-count = { $count ->

[0] Aucun événement récent

[one] { $count } événement récent

*[other] { $count } événements récents

}

More complex with adjective agreement

participant-count = { $count ->

[0] Aucun·e participant·e inscrit·e

[one] { $count } participant·e inscrit·e

*[other] { $count } participant·e·s inscrit·e·s

}

3. Accented Characters and Text Length :warning:

Problem: French text is typically 15-30% longer than English, and accented characters can cause encoding issues.

Example from your template:

<h1>Incubateur Techologique Culturel Québécois</h1>

Pitfalls:

  • UI layouts breaking due to longer text
  • Character encoding issues (Ă©, Ă , ç, etc.)
  • Text overflow in buttons/small spaces

Solutions:

// Ensure proper UTF-8 handling in your filter

env.add_filter("t", move |key: String, args: Value| -> Result<String, minijinja::Error> {

// Always ensure UTF-8 output

let result = locales.format_message(&lang, &key, variables.as_ref())

.unwrap_or_else(|| format!("Missing: {}", key));

// Validate UTF-8 encoding

if !result.is_ascii() {

// Log potential layout issues for long text

if result.len() > key.len() * 1.3 {

tracing::warn!("French translation significantly longer than key: {}", key);

}

}

Ok(result)

});

4. Formal vs. Informal Address (Tu/Vous) :warning:

Problem: French requires choosing between formal (vous) and informal (tu) forms.

Your current context shows informal tone, but this needs consistency:

Pitfall: Mixing formal/informal in the same interface:

Inconsistent

welcome-title = Bienvenue sur notre plateforme # Formal context

create-button = Crée ton événement # Informal "ton"

Solution: Establish a consistent voice:

# Consistent informal (more common for cultural platforms)

welcome-title = Bienvenue sur notre plateforme

create-button = Crée ton événement

profile-edit = Modifie ton profil

# Or consistent formal

welcome-title = Bienvenue sur notre plateforme

create-button = Créez votre événement

profile-edit = Modifiez votre profil

5. Context-Dependent Translations :warning:

Problem: The same English word can have different French translations based on context.

Example: “Event” can be:

  • “ÉvĂ©nement” (general)
  • “Spectacle” (performance)
  • “Manifestation” (cultural event)
  • “Activité” (activity)

Pitfall: Using generic translations:

# Too generic

event = événement

create-event = Créer un événement

**Better approach:**

# Context-specific

event-general = événement

event-cultural = manifestation culturelle

event-performance = spectacle

create-cultural-event = Organiser une manifestation culturelle

6. Date and Time Formatting :warning:

Problem: French date/time formats and conventions differ significantly.

Pitfall: Using English-style formats:

// In your Rust code, handle French date formatting
pub fn format_french_date(date: &DateTime<Tz>) -> String {
    // French uses different day/month names and format
    date.format("%A %d %B %Y Ă  %H:%M").to_string()
}

// In Fluent
event-date = Événement le { DATETIME($date, month: "long", day: "numeric", year: "numeric") }

7. Template Performance Impact :warning:

Problem: Romance languages often require more complex Fluent expressions, impacting performance.

Pitfall: Complex gender/number agreement logic in every template render:

<!-- This gets expensive with many variables -->

{{ "user-count" | t(lang=language, count=users|length, gender=user.gender) }}

**Solution**: Pre-compute complex translations:
// Pre-compute expensive translations in handlers
let user_message = web_context.i18n_context.locales
    .format_message(&language, "user-welcome", Some(&fluent_args! {
        "name" => user.name,
        "gender" => user.gender,
        "count" => user.event_count
    }))
    .unwrap_or_default();

// Pass pre-computed string to template
template_context! {
    user_welcome_message => user_message,
}

8. Quebec French Specificities :warning:

Problem: Your fr-ca locale has Quebec French specificities that differ from European French.

Key differences affecting your event platform:

  • “ÉvĂ©nement” vs “Évènement” (accent)
  • “Fin de semaine” vs “Week-end”
  • “Inscription” vs “Enregistrement”
  • “Courriel” vs “E-mail”

Solution: Maintain Quebec French lexicon:

# Quebec French (fr-ca)

email-label = Courriel

weekend-event = Événement de fin de semaine

register-button = S'inscrire

# European French (fr-fr) would be different

email-label = E-mail

weekend-event = Événement de week-end

register-button = S'enregistrer

Recommended Implementation Strategy for Your Use Case

Based on your Smoke Signal platform, here’s my refined recommendation:

// Enhanced i18n filter for Romance languages

pub fn add_romance_language_filters(env: &mut Environment<'_>, locales: &Locales) {
    let locales_clone = locales.clone();
    
    env.add_filter("t", move |key: String, args: Value| -> Result<String, minijinja::Error> {
        let lang = args.get("lang")
            .and_then(|v| v.as_str())
            .and_then(|s| s.parse().ok())
            .unwrap_or_else(|| "en-us".parse().unwrap());
        
        let mut fluent_args = fluent::FluentArgs::new();
        
        // Handle common Romance language patterns
        if let Some(vars) = args.get("vars") {
            if let Ok(obj) = vars.as_object() {
                for (key, value) in obj.iter() {
                    match key {
                        "count" => {
                            // Ensure proper number handling for Romance plurals
                            if let Some(n) = value.as_u64() {
                                fluent_args.set("count", n);
                            }
                        },
                        "gender" => {
                            // Handle grammatical gender
                            if let Some(g) = value.as_str() {
                                fluent_args.set("gender", g);
                            }
                        },
                        _ => {
                            if let Some(s) = value.as_str() {
                                fluent_args.set(key, s);
                            }
                        }
                    }
                }
            }
        }
        
        let result = locales_clone.format_message(&lang, &key, Some(&fluent_args))
            .unwrap_or_else(|| {
                tracing::warn!("Missing translation for key '{}' in locale '{}'", key, lang);
                format!("[{}]", key)
            });
        
        // Validate for potential UI issues with longer Romance language text
        if lang.to_string().starts_with("fr") && result.len() > key.len() * 1.4 {
            tracing::debug!("French translation much longer than key: {} -> {}", key, result);
        }
        
        Ok(result)
    });
}

This approach addresses the specific challenges of Romance languages while leveraging your existing solid i18n infrastructure.

Ok, after some rounds with Claude, we established a demo framework for i18n.

i’ve created a new route : demo/profile/did:…

and added translations for it.

Some changes had to be made since first test vas just with dud text and infos. There’s a new implementation guide in docs/

Another post another fork.

After toying with fluent and minijinja i was always working against minijinja. So after searching i found fluent-templates which helps a lot.

i started over with new prompts ( thanks nick for inspiration) and i think we are now on the right way.

i18n is almost finished. I need to test every function but looks like fluent-templates was the tool we needed.

i’ll update documentation for develloping new templates with the right syntax and where to put translations.

i also fixed the end date human readable format




@ngerakines

Since the bug squash event is announced, would you like me to put a PR before to also put this into scope or you prefer to wait ?

I can revise the code to be sure it’s cleaned up in about 24 hours top (Probably tonight even) .

I already tried to make a PR but tangled had a bug that prevented me doing it.

Yeah go for it. I’ve made some changes to the templating with recent AIP and delete features and some refactoring of the RSVP form on the event view page.

1 Like

@ngerakines Probably a begginer question but since you made changes, what do you prefer as my branch is based on your last commit 28169a24

Should i redo the work for the new changes or you prefer integrate them yourself and continue adding stuff ?

@ngerakines

Tangled seems to have not corrected the bug i’m experiencing. So i can’t make a proper PR.

This fork is compiling, working as intended, documented and cleaned up. it has a basic event filtering module with it. I think it is better that you just git pull and merge it. it’ll be easier.

I took your lead and made a change to the project that moves to a directory based template structure with ef7b87c.

I was looking through your fork and I think there is a lot of good stuff that I’d like to merge back. I was thinking about the relationship between templates and locales and have a few thoughts.

  1. I definitely want to keep both reloadable and embedded templates in the app.
  2. I think the in-template localization functions can be extremely helpful and I’m very interested in bringing that in to make deep / in-app messages more easily accessible.
  3. I was thinking about template pre-generation and how it could be leveraged.

The idea being that there could be a “generic” template source directory and then as part of the build phase, templates for different languages are generated in ./templates/xxx from them.

That’s a great idea. This would be easier to manage dev. I also splitted ftl files for differents sections but thinking back on it, i think that would be easier to just do one file per language. it’ll help to do comparisons.