How can I extend Linguify for language-dependent images? - Converting linguify context to string

Hi everyone,

I’m currently working on extending the functionality of the linguify package to also handle language-specific images. My goal is to achieve this without explicit conditional logic (like if/else statements), so that users only need to specify the desired language, and the correct image will be displayed automatically.

For those unfamiliar with it, linguify is a package that helps with internationalization by managing language-specific content defined in TOML files.

I’ve successfully configured my TOML file to include paths to different language versions of the same image:

[conf]
default-lang = "en"

[lang.en]
cover_title = "Pancake Machine"
date = "Date"
revision = "Revision"
flag_logo = "Cover/Pictures/Flag_of_the_United_Kingdom_(1-2).webp"

[lang.de]
cover_title = "Pfannkuchenmaschine"
date = "Datum"
revision = "Revision"
flag_logo = "Cover/Pictures/Flag_of_Germany.webp"

[lang.fr]
cover_title = "machine à crêpes"
date = "Date"
revision = "Révision"
flag_logo = "Cover/Pictures/Flag_of_France_(2020â__present).webp"

My idea was to use the linguify function to retrieve the correct image path based on the currently selected language. I envisioned something like this in my template/code:

#image(linguify("flag_logo"))

However, I’ve run into a problem. It seems that the #image() function expects a string representing the image path, while the linguify() function, when used in this context, returns a context object (likely containing more than just the string value).

I’m struggling to figure out how to properly extract the string value (the image path) from the context object returned by linguify() so that it can be used by the #image() function.

Could anyone provide some guidance on how to convert the context returned by linguify("flag_logo") into a simple string containing the image path? Any suggestions or alternative approaches to achieve language-dependent images without explicit logic would be greatly appreciated!

Hi @TobiOneCenobi, welcome and thank you for your question! I have changed your post’s title to bring it in line with the question guidelines and thus make it easier to understand from the title:

Good titles are questions you would ask your friend about Typst.


First of all, the basic problem you’re facing is the following:

Linguify can’t change that, but it can offer an option that requires you to provide context instead of doing that itself. I also help maintain that package, so I can give a bit of insight. The linguify() function is defined here, and the problematic code is the following:

  if from == auto or lang == auto {
    // context is needed to use the default database or current language
    context impl()
  } else {
    impl()
  }

As you can see, you can work around this problem by providing the config and language explicitly, but that’s of course no fun. To support your use case (and have a github account), you could make a PR adding a parameter outside-context: false that, when set to true goes to the else branch. This enables you to get actual strings from the function, at the cost of having to call it as context do-something(linguify(...)).

1 Like

Context values are opaque and there’s no way to access their contents.

Two options I’m thinking of

  1. Remove context inside linguify and require the user to call the function with context. Then it would be like #context image(linguify("flag_logo"))
  2. Allow an optional mapper with linguify, that you pass the result through before returning it, it could look like this:
#linguify("flag_logo", map: image.with(width: 50%))
// just using with to show you can supply arguments in a good way
1 Like

Dear @SillyFreak and @bluss ,

Your suggestions about #context do-something(linguify(...)) and #context image(linguify("flag_logo")) were exactly what I needed to get on the right track.

Building on that, I’ve created a solution by wrapping linguify within my own set of functions. This allows me to read the TOML file, update both my custom state database and the linguify database, and then call my own functions within the Typst context.

Here’s the code I’ve put into linguifyPicture.typ:

#import "@preview/linguify:0.4.2": set-database as linguify-set-database

#let language-database = state("language-database", none)

#let set-language-database(data) = {
  language-database.update(data)
}

#let get-current-language = () => text.lang

#let load-lang(data) = {
  linguify-set-database(data) // Set the database for the @preview/linguify package
  set-language-database(data) // Set the database for our standalone localized-image function
}

#let get-localized-path(key, current-lang) = {
  let lang-data = language-database.get()
  if lang-data == none {
    panic("Language database not set. Call `load-lang()` at the beginning of your document.")
    return none
  }

  let path = none
  let lang-section-name = current-lang

  if lang-section-name in lang-data.lang {
    let lang-section = lang-data.lang.at(lang-section-name)
    if key in lang-section {
      path = lang-section.at(key)
    }
  }

  if path == none and "default-lang" in lang-data.conf {
    let default-lang = lang-data.conf.at("default-lang")
    let default-lang-section-name = "lang." + default-lang
    if default-lang-section-name in lang-data.lang {
      let default-lang-section = lang-data.lang.at(default-lang-section-name)
      path = default-lang-section.at(key, default: none)
    }
  }

  path
}

#let localized-image(key, ..args) = {
  let current-lang = get-current-language()
  let nicePath = get-localized-path(key, current-lang)
  if not nicePath.starts-with("/") and nicePath != none {
    nicePath = "/" + nicePath
  }
  if nicePath != "badPath" {
    image(nicePath, ..args)
  } else {
    panic("Image key '" + key + "' not found for language '" + current-lang + "'.")
  }
}

I am not sure the code above is bulletproof… Here’s how I’m using it in main.typ:

#import "@preview/linguify:0.4.2": * 
#import "Tools/linguifyPicture/linguifyPicture.typ": load-lang, localized-image

#set text(lang: "de")

#let lang-data = toml("Cover/translations.toml")

#load-lang(lang-data) //updates both linguify and linguifyPicture database

#context localized-image("flag_logo", /*image arguments here*/)

One thing I discovered is that Typst doesn’t seem to have built-in support for webp images, so I had to update my TOML file to use .png extensions instead (e.g., "Cover/Pictures/Flag_of_Germany.webp" became "Cover/Pictures/Flag_of_Germany.png").

Thanks again for your helpful insights! This approach seems to be working for my needs.

2 Likes

Here is a simplified version, if you want.

Code
#import "@preview/linguify:0.4.2": set-database as linguify-set-database

#let language-database = state("language-database")

#let set-language-database(data) = language-database.update(data)
#let get-current-language() = text.lang

#let load-lang(data) = {
  linguify-set-database(data) // Set the database for the @preview/linguify package
  set-language-database(data) // Set the database for our standalone localized-image function
}

#let get-localized-path(key, current-lang) = {
  let lang-data = language-database.get()
  if lang-data == none {
    panic("Language database not set. Call `load-lang()` at the beginning of your document.")
  }

  let path
  let lang-section-name = current-lang

  if lang-section-name in lang-data.lang {
    let lang-section = lang-data.lang.at(lang-section-name)
    if key in lang-section {
      path = lang-section.at(key)
    }
  }

  if path == none and "default-lang" in lang-data.conf {
    let default-lang = lang-data.conf.at("default-lang")
    let default-lang-section-name = "lang." + default-lang
    if default-lang-section-name in lang-data.lang {
      let default-lang-section = lang-data.lang.at(default-lang-section-name)
      path = default-lang-section.at(key, default: none)
    }
  }

  path
}

#let localized-image(key, ..args) = {
  let current-lang = get-current-language()
  let nicePath = get-localized-path(key, current-lang)
  if not nicePath.starts-with("/") and nicePath != none {
    nicePath = "/" + nicePath
  }
  if nicePath == "badPath" {
    panic("Image key '" + key + "' not found for language '" + current-lang + "'.")
  }
  image(nicePath, ..args)
}

I also see that you know how to write a proper code block for Typst snippets. Could you please fix the code block in the OP too?

1 Like

That’s some really well-structured code! I seem to have lost the ability to edit my post after @SillyFreak kindly updated the title.

1 Like

there’s a time limit for relatively new users of one day. I have fixed the syntax highlighting, which is what I think Andrew was referring to.