Wes Byrne • Mar 2nd, 2022
Icons and images are in just about every product these days and Lingo is no exception. We use icons in all of our apps for interface elements like buttons and informational badges to minimize clutter and make things easily recognizable.
Like many aspects of building software products, adding new icons or making changes to existing ones is often a fairly manual process which can be clunky at best. Over time things can go wrong – maybe a new designer exports new assets at a different size, format, or using a different naming convention causing confusing in the handoff to code.
We wanted to see if we could simplify this process and reduce the margin for error by automating with the Lingo API.
Here's the process we came up with.
Our design team uses Figma. They created all of our icons as components in a library file called "Icon Library". This allows the design team to pull these icon components into other Figma files when building product designs ensuring consistency.
How you want to organize isn't important for this article but here is a look at our Icons file.
With our icons ready to go in Figma, getting them into Lingo using our integration took only a few clicks. If you haven't tried our Figma integration yet you can learn about it in Introducing Lingo's Figma Integration.
With all of the icons added to our kit it was time to do some organization. We added some information about the design principles used to create icons and instructions on how to use them using guides, notes and supporting content. We also went ahead and added our Mac app icons to their own section.
So now we have 3 sections in our kit:
Before we continue there are a few details about our library that will come into play later when using it in the code.
To help organize our library we grouped them into categories such as: `action`, `content`, `info`, etc. Each category is marked by a heading in the "Interface Icons" section. You may have noticed these groups in Figma above and in they will be used to reference the icons in code.
We wanted our icons to look great at all sizes. Most icons are shown at 24x24 but many are shown at 16x16. While we could shrink the SVGs down using Lingo's export options, in some cases that will leave things looking a little fuzzy, especially in icons with more detail.
To solve this we actually have 2 versions of each icon, one pixel aligned at 24px and another at 16px. Both are exported to Lingo. Remember, they are actually different designs like the trash icon here, so we can't use Lingo's resizing features.
So our icons have made the journey from Figma to Lingo but ultimately we need them in our codebase. This is where your needs may start to differ from ours but with some coding knowledge you can adjust as needed.
Our React web app uses a sprite sheet and an Icon component to render Icons. We want to generate that sprite sheet with all the icons in our "Interface Icons" section then bundle that into our codebase.
LingoJS, our javascript SDK for accessing the Lingo API made this easy. Let's take a look at the code:
// We load our API key from a local config file. 
// As always avoid checking it into your version control.
const token = "" // Your API key, 
    section = "2B0D2DE5-9DB0-4582-BDAC-A168FF720172", // Interface Icons
    spaceID = "53411";
Lingo.setup(spaceID, token);
const name = "icons";
const items = await fetchCategories(section);
const sprite = await generateSprite(items, name);
const spritePath = path.resolve(__dirname, "resources");
saveFile(sprite, spritePath + `/${name}.svg`);From a high level this is what we are going to do
So let's look at how we fetch the data from Lingo.
async function fetchCategories(section: string): Promise<IconSource[]> {
  const items = await Lingo.fetchAllItemsInSection(section);
  return items.filter(item => item.type === ItemType.Asset && 
                    item.asset.type === AssetType.SVG)
}Thank you LingoJS!
While that would indeed return a list of SVG assets, remember the details about grouping our icons into categories and how we have two sizes of each icon? Of course, if you don't need all that you can keep it simple, but here is our actual implementation for inspiration.
type IconSource = {
  uuid: string;
  assetId: string;
  dimensions: string;
  name: string;
};
async function fetchCategories(section: string): Promise<IconSource[]> {
  const items = await Lingo.fetchAllItemsInSection(section);
  
  // Append _16 to the smaller 16x16 assets
  function symbolName(category: string, item: Item) {
    let name = item.asset.name;
    if (item.asset.dimensions === "16x16") {
      name += "_16";
    }
    return `${category}${name}`;
  }
  // Keep track of which category we are in as we iterate the items
  let currentCategory = "";
  return items.reduce((res, item) => {
    if (item.type === ItemType.Heading) {
      currentCategory = item.data.content;
    } else if (item.type === ItemType.Asset && item.asset.type === AssetType.SVG) {
      res.push({
        uuid: item.id,
        assetId: item.assetId,
        dimensions: item.asset.dimensions,
        name: symbolName(currentCategory, item),
      });
    }
    return res;
  }, []);
}We fetch all the items then group them by category.
To allow referencing the different sizes of each icon we also use the dimensions of the asset to append `_16` to the names of the smaller variations.
If you're wondering why we don't name the assets like this in Lingo, it's because we don't need it there. Each asset has dimensions and they are easily visible in Lingo, adding _16 to the name of each asset would just be duplicating metadata.
async function donwloadItems(items: [Item]): Promise<Buffer[]> {
  const downloads = items.map(async item => await Lingo.downloadAsset(item.assetId));
  return await Promise.all(downloads);
}
async function generateSprite(items, name = "icons") {
  // Download the files for each asset
  const files = await donwloadItems(items);
 
  // Strip the SVG tags, wrap in named symbol tags
  const symbols = files
    .map(f => f.toString("utf-8"))
    .map(s => s.replace(/<svg [^>]*>/, ""))
    .map(s => s.replace("</svg>", ""))
    .map((symbol, idx) => {
      const item = items[idx];
      return `<!-- ${item.name} --><symbol id="${item.name}">${symbol}</symbol>`;
    })
    .join(""); 
  // Wrap the symbols in an SVG tag and 
  const svgString = `<svg id="${name}" style="display:none;"><defs>${symbols}</defs></svg>`;
  // We prettify the XML for nicer diffs but this isn't necessary.
  return prettifyXml(svgString);
}Before we can generate the sprite sheet we download all the actual SVG data for each icon.
There are a few steps here to clean the SVGs for each asset then they are compiled into symbols and put into a containing SVG tag.
Again, if you don't need a sprite sheet you could do whatever you like with the data for each asset. Be that an SVG, image or document. You could even use the filecuts API to get images in different types or sizes.
Here's what our sprite sheet looks like.
As mentioned earlier we have a custom component to render these icons. Notice the category names being used with dot notation to reference the different icons and thanks to a bit of logic, the size variations are automatically selected based on the size props.
<!-- Defaults to 24 and uses the large variation of -->
<Icon iconId="action.trash" />;
<!-- Uses the smaller variation -->
<Icon iconId="action.trash" size="16" />;
<!-- Uses the larger variation for sizes > 24 -->
<Icon iconId="action.trash" size="40" />;I hope this inspires you to help simplify your team's processes. With the Figma integration and less than 150 lines of code we turned a complex error prone process into a few clicks.
Of course we could even go a step farther and automate running the script to get the latest icons from Lingo but for now we run that manually since they don't change that frequently.
If you're interested in using the Lingo API head over the developer.lingoapp.com to read our docs. You can also view our live Icon Library kit.
If you have any questions or need help getting started feel free to contact us, we're happy to help.