Elixir Atoms

One of the stranger bits of syntax I see in Elixir is for Atoms.

To quote the previous link “Atoms are constants whose values are their own name”.

What does this mean ?

Well imagine you were writing code such as

configuarion = "configuration"
ok = "ok"

Essentially the value is the same as the name, so basically instead if assigning a value of the same name we simply prefix the name with : to get the following

:configuration
:ok

In fact :true, :false and :nil are atoms but we would tend to use the alias of these, i.e. without the colon, true, false and nil.

The syntax for atoms is that they start with an alpha or underscore, can contain alphanumeric characters, can also contain @ and can end with and alphanumeric character or either ? or !. If you wish to create an atom with violates this syntax you can simply enclose in double quotes, i.e.

:"123atom"

and to prove it we can use

is_atom(:"123atom")

Atoms can also be declared starting with an uppercase alpha character such as

Atom
is_atom(Atom)

Atoms with the same content are always equivalent.

Modules are represented as atoms, for example :math is the module math and :”Elixir.String” is the same as a String.

Elixir and Phoenix

Most languages which I start to learn, I’ve found I learn the basics of the language (enough to feel at relative ease with the language) but then want to see it in real world scenarios. One of those is usually a web API or the likes. So today I’m looking at using Elixir along with the Phoenix framework.

Note: I’m new to Elixir and Phoenix, this post is based upon my learnings, trying to get a basic web API/service working and there may be better ways to achieve this that I’m not aware of yet.

Phoenix is a way to (as they say on their site) to “build rich, interactive web applications”. Actually I find it builds too much as by default it will create a website, code for working with a DB (PostgresQL by default) etc. In this post I want to create something more akin to a web API or microservice.

If you’re after the default application, then run

mix phx.server

In this post I want to create a simple API service so instead we’ll use phx.new to create a service named my_api and well remove the website/HTML and ecto (the DB) side of things

mix phx.new my_api --no-html --no-ecto --no-mailer

If you run the command above you’ll get a new application generated. Just cd my_api to allow us to run the service etc.

If you’d like to see what the default generated application is then run the following

mix phx.server

By default this will start a server against localhost:4000. If you open the browser you’ll see a default dashboard/page which likely says there’s no route for GET / and then lists some available routes.

The /dev/dashboard route takes you to a nice LiveDashboard showing information about the Elixir and Phoenix.

To shutdown the Phoenix server CTRL+C twice within the terminal that you ran it up from.

For my very simple web service, I do not even what the live dashboard. So if you created that new app. delete your new app folder and then run this minimal code version (unless you’d prefer to keep live dashboard etc.)

mix phx.new my_api --no-html --no-ecto --no-mailer --no-dashboard --no-assets --no-gettext

This will then generate a fairly minimal server which is a good starting point for our service. You’ll notice first off that there are now, no routes when you run this via mix phx.server
.

Let’s add a controller, this will acts as the controller for our web service, so within the /lib/my_api_web/controllers folder add a new file named math-controller.ex and past the following code into it (obviously change the module name to suite your application name)

defmodule MyApiWeb.MathController do
  #use MyApiWeb, :controller
  use Phoenix.Controller, formats: [:html, :json]

  def index(conn, _params) do
   json(conn, "{name: Scooby}")
  end
end

We now need to hook up our controller to a route, so go to the router.ex file within the /lib/my_api_web/ folder and alter the scope section to look like this

scope "/", MyApiWeb do
  pipe_through :api

  resources "/api", MathController, except: [:new, :edit, :create, :delete, :update, :show]
end

If you run mix phx.server you should see a route to /api, typing http://localhost:4000/api will return “{name: Scooby}” as defined in the math-controller index. This is not very math-like so let’s create a couple of functions, one for adding numbers and one for subtracting.

Remove the resources section (or comment out using #) in the scope then add the following routes

get "/add", MathController, :add
get "/sub", MathController, :subtract

Go to the math-controler.ex and add the following functions

def add(conn, %{"a" => a, "b" => b}) do
  text(conn, String.to_integer(a) + String.to_integer(b))
end

def subtract(conn, %{"a" => a, "b" => b}) do
  text(conn, String.to_integer(a) - String.to_integer(b))
end

Notice we destructuring params to values a and b – we’ll convert those values to integers and use the text function to return raw text (previously we expected JSON hence uses the json function). Now when you browse the add method, for example http://localhost:4000/add?a=10&b=5 or subtract method, for example http://localhost:4000/sub?a=10&b=5 you should see raw text returned with answers to the math functions.

What routes are we exposing

Another useful way of checking the available routes (without running the server) is, as follows

mix phx.routes

Config

If you’ve looked around the generated code you’ll notice the config folder.

One thing you might like to do now is change localhost to 0.0.0.0 so edit dev.exs and replace

http: [ip: {127, 0, 0, 1}, port: 4000],

with

http: [ip: {0, 0, 0, 0}, port: 4000],

If you do NOT do this and you decide to deploy the dev release to Docker, you’ll find you cannot access your service from outside of Docker (which ofcourse is quite standard).

Releases

Generating a release will precompile any files that can be compiled and allows us to run the server without the source code (as you’d expect) you will need to tell the compiler what configuration to use, we do that by setting the MIX_ENV like this

export MIX_ENV=prod

(No MIX_ENV environment variable will default dev)

Then running

mix release

This will create and assemble your compiled files to _build/prod/rel/my_api/bin/my_api (obviously replacing the last part with your app name). The results of a release build show using

Note: Replace /prod/ with /dev/ above etc. as per the environment you’ve compiled for

_build/prod/rel/my_api/bin/my_api start

to start your application, this will need start a server. By default the above does not start a server so instead we need to set the following environment variable

export PHX_SERVER=true

You’ll also able to run following it will automatically generate the bin/server and sets the PHX_SERVER environment variable

mix phx.gen.release

One last thing, you may find when you use the start command (against PROD) that this fails saying you are missing the SECRET_KEY_BASE. We can generate this using

mix phx.gen.secret

Then simply

export SECRET_KEY_BASE=your-generated-key

This is for signing cookies etc. and you can see where the exception comes from within the runtime.exs file. This is set as an environment variable, best not to check into source control.

Dockerizing our service

Okay, it’s not Elixir specific, but I feel that the natural conclusion to our API/service development is to have it all running in a container. Let’s start by creating a container image based upon the build and using the phx.server call…

Create yourself a Dockerfile which looks like this

FROM elixir:latest

RUN mkdir /app
COPY . /app
WORKDIR /app

RUN mix local.hex --force
RUN mix do compile

EXPOSE 4000

CMD ["mix", "phx.server"]

I’m assuming we’re going to stick with port 4000 in the above and in the commands below, so I’ll document this via the EXPOSE command.

Now to build and run our container let’s use the following

docker build -t pp/my-api:0.1.0 .
docker run --rm --name my-api -p 4000:4000 -d pp/my-api:0.1.0

Now you should be able to uses http://localhost:4000 to access your shiny new Elixir/Phoenix API/service.

Note: Remember that if you cannot access the service outside of the docker image, ensure you’ve set the http ip in dev.exs to 0.0.0.0

If we want to instead containerize our release build then we could use the following

FROM elixir:latest

ENV PHX_SERVER=true

RUN mkdir /app
COPY /_build/dev/ /app
WORKDIR /app/rel/my_api/bin

EXPOSE 4000

CMD ["./my_api", "start"]

Again using the previous build and run commands, will start the server (if all went to plan).

Code

Code is available in my GitHub blog project repo.

A little more Elixir, we’re talking modules and functions

In my post Starting out with Elixir I started out looking at how to create a basic Elixir file then looked at mix to generate a project.

Again, I’m not going to go into any depth on the Elixir language, there’s no way I could cover it, but let’s look at modules and functions.

NOTE: I’m new to Elixir, so take all information in this post as me learning things – there may be better ways or different ways to do things that I’ll learn later. This is all about getting a grounding in some basic concepts.

Modules

Like modules or namespaces in other languages, Elixir has the module concept for grouping together functions. For example when using IO.puts, IO is the module and the function is puts

To declare a module we write the following

defmodule Module.Name do
  # your functions
end

The module name can have a full stop/period in the names. The module’s first letter should be uppercase. We can group together functions within a module including private functions.

Functional means functions

Writing anything but the simplest applications/code requires the need for functions. Elixir is a functional language so let’s create some.

Named functions are defined using the following format and should be placed within a module and the function name should start with a lowercase letter, for example

defmodule Simple.Messages do
  def say_hello() do 
    "Hello World"
  end
end

Like other functional languages we can just have the return value as the last line of the function. So in this example we are returning a string “Hello World”.

We could rewrite the above function like this as a one liner

def say_hello(), do: "Hello World"

Actually we can using a syntax such as

def say_hello(), do: (
    "Hello World"
)

and having multiple lines within the ( … ) parenthesis, the def … end style shown initially is a syntactic sugar way of declaring your functions.

We pass parameters in the “standard” way, within the parenthesis of the function and we can supply default values. Like other functional languages, we do not need to define the types of parameters, these are inferred (again, pretty standard in the functional world).

def say_hello(name), do: "Hello #{name}"

We can create function overloads, i.e. functions with the same name but different arity (number of parameters or arity). The example above also shows string interpolation using #{} syntax.

Here’s an example with a default parameter (obviously this will display a warning if you have a parameter-less function of the same name

def say_hello(name \\ "World"), do: "Hello #{name}"

We can also create anonymous functions, for example

anon = fn (name) -> "Hello #{name}" end
# calling the function is slightly different to named functions
IO.puts anon.("Scooby")

There’s also a shorthand for such functions where we denote that parameters using $ followed by the parameter index, i.e.

anon = &("Hello #{&1}")
IO.puts anon.("Scooby")

As mentioned in the modules section, we can also create private functions and these are declared using defp like this

defp say_hello(name), do: "Hello #{name}"
def say_hello_scooby(), do: say_hello("Scooby")

In this example say_hello is private and say_hello_scooby is public.

Aliasing module names

In some cases we might want to alias a module. For example out Simple.Messages module might be alias within another module, where by

defmodule HelloWorld.Application do
  alias Simple.Messages

  def run() do
     IO.puts Message.say_hello("Scooby")
  end
end

Notice we no longer need to fully qualify the module name when it’s used.

That should be enough to get one started writing modules and functions, I’m sure I’ll create other posts to explore how these work further at some point.

Starting out with Elixir

I’ve wanted to try out Elixir for a while. The Elixir language is a functional, dynamic language runs on the Erlang VM.

Obviously this is a small post and hence we’re going to cover very little of the Elixir language here, instead we’ll cover the basics of getting things up and running.

We’re going to run up a an Elixir environment using devcontainers.

  • Create yourself a .devcontainer folder within your source folder
  • Create a file named devcontrainer.json with the following contents
    {
        "image": "elixir",
        "forwardPorts": [3000]
    }
    
  • Open Visual Code from the folder (or open the folder in VS Code)
  • You should have the option to open as a devcontainer, so do that

I’d suggest installing the ElixirLS: Elixir support and debugger or another plugin if you prefer.

Hello World

As is the usual starting point of any language, let’s create a hello_world.exs file and add the following

IO.puts("Hello World")

Now to run this open a terminal from VS Code and type.

elixir hello_world.exs 

As you can see the IO.puts function outputs to the console and strings are represented by double quotes.

The Mix build tool

Mix is a little like the dotnet command (if you come from .NET) in that it can be used to create a new project, as well as different types of project. It’s used to run unit tests and ofcourse compile our application.

Let’s start by creating a new Elixir project

mix new my_project

This will create a new project named my_project along with default files such as mix.exs (using for configuring our application, dependencies etc.). We also have a test folder with an example test

defmodule ExampleTest do
  use ExUnit.Case
  doctest Example

  test "greets the world" do
    assert Example.hello() == :world
  end
end

We can run the tests using

mix test

We can compile our Elixir application using

mix compile

this will produce a _build folder and within this we’ll see a ebin/my_project.app

Supervisor

Now, I’m going to state upfront, at this time all I know about supervisors and supervision trees is that they’re like an OS in a lightweight process. They start, work, then terminate. This is the mechanism we’ll use to create a Hello World application using mix

Run

mix new hello_world --sup

This produces a mix.exs file with the key addition

def application do
  [
    extra_applications: [:logger],
    mod: {HelloWorld.Application, []}
  ]
end

and the lib/hello_world/application.ex file looks like this. I’ve added the IO.puts line as well as removed the comments

defmodule HelloWorld.Application do
  @moduledoc false

  use Application

  @impl true
  def start(_type, _args) do
    children = [
  ]

   IO.puts "Hello World"

   opts = [strategy: :one_for_one, name: HelloWorld.Supervisor]
   Supervisor.start_link(children, opts)
  end
end

This will then run a process (using mix run) and output our “Hello World” string then terminates cleanly.

exs and ex files

You’ll notice that both .ex and .exs are used for Elixir file extensions. The basis seems to be that .ex are meant to be compiled whereas .exs are script files. It can be a little confusing as mix generated projects include both. For example for config and tests it generates .exs files, for the endpoints, router etc. they’re .ex.

References

Elixir
Mix
Using Supervisors to Organize Your Elixir Application

StringSyntaxAttribute and the useful hints on DateTime ToString

For a while I’ve used the DateTime ToString method and noticed the “hint” for showing the possible formats, but I’ve not really thought about how this happens, until now.

Note: This attribute came in for projects targeting .NET 7 or later.

The title of this post gives away the answer to how this all works, but let’s take a look anyway…

If you type

DateTime.Now.ToString("

Visual Studio kindly shows a list of different formatting such as Long Date, Short Date etc.

We can use this same technique in our own code (most likely libraries etc.) by simply adding the StringSyntax attribute to our method parameter(s).

For example

static void Write(
   [StringSyntax(StringSyntaxAttribute.DateOnlyFormat)] string input)
{
    Console.WriteLine(input);
}

This attribute does not enforce the format (in the example above), i.e. yo can enter whatever you like as a string. It just gives you some help (or hint) as to possible values. In the case of the DateOnlyFormat these are possible date formatters. StringSyntax actually supports other syntax hints such as DateTimeFormat, GuidFormat and more.

Sadly (at least at the time of writing) I don’t see any options for custom formats.

Dockerize your React application

You’ve created you React application and are now looking to create a docker image for it.

Before we look at the Dockerfile, create yourself a .dockerignore file that looks like this

.git
node_modules

We do not require the .git folder in our image and we’re going to install our node modules via npm install as you’ll find copying the node_modules folder(s) is slow.

Let’s jump straight in and look at a Dockerfile that will take our React code containerize it and set the container up to run nginx to host it.

FROM node:21-alpine3.18 as build

WORKDIR /usr/app
COPY . .
RUN npm install
RUN npm run build

FROM nginx:alpine
COPY --from=build /usr/app/build /usr/share/nginx/html
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

We’re using a node alpine image as our base. This is a lightweight image as we won’t our image to be lean. Next we create our WORKDIR, this can be set to your preferred location but just remember to reuse that location in the COPY command. We’re using .dockerignore to ignore .git and node_modules which allows us to then just copy everything to the image, where we then install then build our React application.

Finally we use nginx to serve our application, first copying the build folder to nginx and finally we set up the CMD to run nginx once the container is started, thus hosting our React application.

To build our image we can just use the following (change the tag name and version to suit your application)

docker build -t my-app:0.1.0 .

and to run, again set your application name any port redirects and use you tagged and version

docker run --rm --name my-app -p 8080:80 -d my-app:0.1.0

We could also extend this sample to include copying of nginx.conf to the image, for example if you want the supply a config like this

worker_processes 4;

events { worker_connections 1024; }

http {
  server {
    listen 4200;
    root  /usr/share/nginx/html;
    include /etc/nginx/mime.types;

  location / {
    root   /usr/share/nginx/html;
    index  index.html;
    try_files $uri $uri/ /index.html;
  }
}
}

If we assume we’re storing out nginx.conf file in a folder named .ngnix (this is not required it’s just for this example) then we could add the following to the Dockerfile, after the line FROM nginx:alpine add the following and whilst we’re at it let’s get rid of the default files that might be located in the images nginx/html folder

COPY ./.nginx/nginx.conf /etc/nginx/nginx.conf
RUN rm -rf /usr/share/nginx/html/*

Docker Compose

Whilst we’re here, let’s create a simple docker-compose.yaml file for our new image.

version: '3.8'
services:
  front-end:
    build:
      context: ./ui
      dockerfile: ./ui/Dockerfile
    ports:
      - 4200:4200
    image: putridparrot/my-app:0.1.0
    container_name: my-app

We might like to store configuration for this image on the hosting server, i.e. via a volume in which case we’d simply add to the bottom of this file the following

    volumes:
      - ./ui/public/appsettings.json:/usr/share/nginx/html/appsettings.json

In this example we’re using an appsettings.json file to configure the environment, or it might include feature flag settings or whatever and assuming it’s stored in the public folder of you React application.

Now we just docker-compose up.

Zustand state management

I’m used to using Redux and more recently Redux Toolkit for global state management in React, however along with state management libraries such as Mobx there’s another library of interest to me, named Zustand. Let’s see how to set up and project and use Zustand and take a very high level look at how to set-up a project with Zustand…

Create yourself a React application, as usual I’m using TypeScript.

  • Add Zustand using yarn add zustand
  • Our store is a hook, and to create the store we use the create method, for example
    import { create } from "zustand";
    
    interface CounterState {
        counter: number;
        increment: () => void;
        decrement: () => void;
    }
    
    export const useCounterStore = create<CounterState>(set => ({
        counter: 0,
        increment: () => set(state => ({ counter: state.counter + 1 })),
        decrement: () => set(state => ({ counter: state.counter - 1 })),
    }));
    

The above creates a simple store, with state and methods to interact with the state. As we’re using TypeScript, we’ve declared the interface matching our state.

To use this state we simply use the hook like this (change App.tsx to look like the following)

import './App.css';
import { useCounterStore } from "./store";

function App() {
  const { counter, increment, decrement } = useCounterStore();

  return (
    <div className="App">
      <button onClick={increment}>+</button>
      <div>{counter}</div>
      <button onClick={decrement}>-</button>
    </div>
  );
}

export default App;

We can also get slices of our state using the hook like this

const counter = useCounterStore(state => state.counter);

Before we move on, unlike RTK we need to enable redux devtools if we want to view the state in the Redux DevTools in our Browser, so to add the dev tool extensions do the following

  • yarn add @redux-devtools/extension
  • We need to import the devtools and change our store a little, so here’s the store with all the additions
    import { create } from "zustand";
    import { devtools } from "zustand/middleware"
    import type {} from "@redux-devtools/extension";
    
    interface CounterState {
      counter: number;
      increment: () => void;
      decrement: () => void;
    }
    
    export const useCounterStore = create<CounterState>()(
      devtools (
        set => ({
          counter: 0,
          increment: () => set(state => ({ counter: state.counter + 1 })),
          decrement: () => set(state => ({ counter: state.counter - 1 })),
        }),
        {
          name: "counter-store",
        }
      )
    );
    

Zustand also has the ability to wrap our global state within a persistence middleware. This allows us to save to various types of storage. We simply wrap our state in persist like this

import { devtools, persist } from "zustand/middleware"

export const useCounterStore = create<CounterState>()(
  devtools (
    persist (
      set => ({
        counter: 0,
        increment: () => set(state => ({ counter: state.counter + 1 })),
        decrement: () => set(state => ({ counter: state.counter - 1 })),
      }),
      {
        name: "counter-store",
      }
    )
  )
);

By default (as in the above code) this state will be persisted to localStorage.

Go check your Application | Local Storage in Edge or Chrome developer tools, for example for Local Storage I have a key counter-store with the value {“state”:{“counter”:4},”version”:0}.

Code

Code from this post is available on github.

React with Signals

Signals are another way of managing application state.

You might ask, “well great but we already have hooks like useState so what’s the point?”

In answer to the above perfectly valid question, Signals work on a in a more granular way – let’s compare to useState. If we create a simple little (and pretty standard) counter component we can immediately see the differences.

Create yourself a React application, I’m using yarn but use your preferred package manager.

yarn create react-app react-with-signals --template typescript

Add the signal package using

yarn add @preact/signals-react

Now we’ll create a folder named components and add a file named CounterState.tsx which looks like this

import { useState } from "react";

export const CounterState = () => {
  const [count, setCount] = useState(0);

  console.log("Render CounterState");

  return (
    <div>
      <div>Current Value {count}</div>
      <div>
        <button onClick={() => setCount(count - 1)}>Decrement</button>
        <button onClick={() => setCount(count + 1)}>Increment</button>
      </div>
    </div>
  );
}

It’s not very pretty but it’s good enough for this demonstration.

Finally create a new file in the components folder named CounterSignals.tsx which should look like this

import React from 'react';
import { useSignal } from "@preact/signals-react";

export const CounterSignals = () => {
  const count = useSignal(0);

  console.log("Render CounterSignals");

  return (
    <div>
      <div>Current Value {count}</div>
      <div>
        <button onClick={() => count.value--}>Decrement</button>
        <button onClick={() => count.value++}>Increment</button>
      </div>
    </div>
  );
}

As you can see, the Signals code creates a Signals object instead of the destructuring way used with useState and the Signals object will not change, but when we change the value, the value within the object changes but does not re-rendering the entire component each time.

Let’s see this by watching the console output in our preferred browser, so just change App.tsx to look like this

import React from ‘react’;
import { CounterState } from ‘./components/CounterState’;
import { CounterSignals } from ‘./components/CounterSignals’;

function App() {
return (


);
}

export default App;
[/em]

Start the application. You might wish to disable React.StrictMode in the index.tsx as this will double up the console output whilst in DEV mode.

Click the Increment and Decrement buttons and you’ll see our useState implementation renders on each click whereas the counter changes for the Signals version but no re-rendering happens.

Code

Code for this example using Signals can be found on github.

i18n in React

i18n (internationalization) is the process of making an application work in different languages and cultures. This includes things like, translations of string resources through to to handling date formats as per the user’s language/culture settings, along with things like decimal separators and more.

There are several libraries available for helping with i18n coding but we’re going to focus on the react-i18next in this post, for no other reason that it’s by other teams in the company I’m working for at the moment.

Getting Started

The react-i18next website is a really good place to get started and frankly I’m very likely to cover much the same code here, so their website should be your first port of call.

If you’re want to instead follow thought the process here then, let’s create a simple React app with TypeScript (as is my way) using

npx create-react-app i18n-app --template typescript

Now add the packages react-i18next and i18next i.e.

npm install react-i18next i18next --save

In this Getting Started I’m going to also include the following libraries which will handle loading the translation files and more

npm install i18next-http-backend i18next-browser-languagedetector --save

Adding localized strings

We’re going to first show an example of embeding the string into a .ts file, however it’s much more likely we’ll want them in a separate file (or multiple files). Let’s get this started by creating a file named i18n1.ts.

In the file we’ll just write one string and here it is

import i18n from "i18next";
import { initReactI18next } from "react-i18next";
import LanguageDetector from "i18next-browser-languagedetector";

i18n
  .use(LanguageDetector)
  .use(initReactI18next)
  .init({
    resources: {
      en: {
        translation: {
          welcome: "code: Hello string en World"
        },
      },
      fr: {
        translation: {
          welcome: "code: Bonjour string fr World"
        }
      }
    },
    debug: true,
    interpolation: {
      escapeValue: false, 
    },
  });

export default i18n;

As you can see we’re using the LanguageDetector to automatically set our resources. On my browser the language is set to en-GB so my expectation is to see the en strings as I’ve not listed en-GB specific strings.

IMPORTANT: Before we can use this, go to index.tsx and import this file import “./i18n1”; so the bundler includes it.

Display/using our localized strings

Now we’ll need to actually use our translation strings. react-i18next comes with hooks, HOC’s and standard JS type functions to interact with our translated strings. Let’s clear out most of the App.tsx and make it look like this

import React from "react";
import "./App.css";
import { useTranslation } from "react-i18next";

const lngs: any = {
  en: { nativeName: "English"},
  fr: { nativeName: "French"},
}

function App() {
  const { t, i18n } = useTranslation();

  return (
    <div className="App">
      <div className="App-header">
        {t("welcome")}
        <div>
          {Object.keys(lngs).map(lng => {
            return <button key={lng} style={{margin: "3px"}}
              onClick={() => i18n.changeLanguage(lng)} disabled={i18n.resolvedLanguage === lng}>{lngs[lng].nativeName}</button>
          })}
        </div>
      </div>
    </div>
  );
}

export default App;

This code will simply display the English/French buttons to allow us to change the language as we wish. Notice, however, that if we add a new language, such as de: { nativeName: “German”} and no strings exist for that language, you’ll end up seeing the “key” for the string. We can solve this later.

At this point if all is working, you can start the application up and switch between the languages. You’ll see that the strings will be prefixed with code: just to make it clear where the strings are coming from, i.e. our code file.

Moving to resource type files (part 1)

As I mentioned, we probably don’t want to embed our string in code. It’s preferable to move them into their own .JSON files.

Create a folder within the src folder named locales (the names of the folders and files doesn’t really matter but it’s good to be consistent) and within that we’ll have one folder name en-GB and another named fr. So the English strings are specific to GB but the French covers all French languages locales.

Now in en-GB create the file translations.json which will look like this

{
  "welcome": "src: Hello en-GB World"
}

For the French translations, add translations.json to the fr folder and it should look like this

{
 "welcome": "src: Bonjour fr World"
}

Note the src: prefix is again, just there to allow us to see where our resources are coming from in this demo.

We now need to change our i18n.ts file to look like this

import i18n from "i18next";
import { initReactI18next } from "react-i18next";
import LanguageDetector from "i18next-browser-languagedetector";

import enGB from "../src/locales/en-GB/translation.json";
import fr from "../src/locales/fr/translation.json";

const resources = {
  en: {
    translation: enGB
  },
  fr: {
    translation: fr
  }
};
i18n
  .use(LanguageDetector)
  .use(initReactI18next)
  .init({
    resources,
    debug: true,
    interpolation: {
      escapeValue: false, 
    },
  });

export default i18n;

So in this code we’reimporting the JSON and then assigning to the resources const. This is very similar to the other way we bought the resources into the i18n.ts file, just we’re importing via JSON files.

Moving to resource type files (part 2)

There’s another way to import the translated strings and that is to simply include the files in the public folder of our React application. So in the public folder add the same folders and files, i.e. locales folder with en-GB and fr folders with the same translation.json files as the last example. I’ve changed the src: prefix on the strings to public: again just so I can prove, for this demo, where the strings originate from.

In other words, here’s the en-GB translations.json file

{
  "welcome": "public: Hello en-GB World"
}

and the fr translations.json file

{
  "welcome": "public: Bonjour fr World"
}

Now back in our i18n.ts file change it to look like this

import i18n from "i18next";
import { initReactI18next } from "react-i18next";
import Backend from "i18next-http-backend";
import LanguageDetector from "i18next-browser-languagedetector";

i18n
  .use(Backend)
  .use(LanguageDetector)
  .use(initReactI18next)
  .init({
    fallbackLng: {
      "en" : ["en-GB"]
    },
    debug: true,
    interpolation: {
      escapeValue: false, 
    },
  });

export default i18n;

Fallback

My browser is setup as English (United Kingdom) which is en-GB and all works well. But what happens if we add a detect a language where we have no translation strings for? Well let’s try it by adding a German option to the lngs const in our App.tsx, so it looks like this

const lngs: any = {
  en: { nativeName: "English"},
  fr: { nativeName: "French"},
  de: { nativeName: "German"},
}

Now, what happens ? Well we’ve see the key for the string, which is probably not what we want in production, better to fall back to a known language. So we need to change the i18n.ts file and add a fallbackLng, I’ll include the whole i18n object so it’s obvious

i18n
  .use(LanguageDetector)
  .use(initReactI18next)
  .init({
    resources,
    lng: "en-GB",
    fallbackLng: "en",
    debug: true,
    interpolation: {
      escapeValue: false, 
    },
  });

Now if our application encounters a locale it’s not setup for, it’ll default to the fallback language (in this case) English. We can also achieve this using

fallbackLng: {
  "default": ["en"]
},

Using this syntax we can also map different languages to specific fallback languages, so for example we might map Swiss locale to map to use French or Italian. This is achieved by passing an array of fallback languages (as taken from the <a href="https://www.i18next.com/principles/fallback" rel="noopener" target="_blank">Fallback documentation</a>

[code]
fallbackLng: { 
  "de-CH": ["fr", "it"], // French and Italian are also spoken in Switzerland
},

Finally with regards fallback languages, we can write code to determine the translation to use, again the Fallback documentation has a good example of this, so I’d suggest checking that link out.

Code

Code for this post is available on github which includes i18n files 1-3 for each of these options for creating translations. just change the index.tsx to import the one you wish to use.