In one of his posts, Resti, the creator of Juris showed how easy it is to implement I18n in Juris.
You absolutely don't need to "wrap" all your application inside any wrapper.
Simply code an I18n component, which will be injected in all components via context parameter.
Here is the code in charge of the translation:
// --- 1. THE SERVICE (Headless Component) ---
/**
* @name I18nManager
* @description A headless component that provides internationalization (I18n) services.
* It contains no UI itself; its only job is to manage translation logic and expose
* an API (`t`) that other components can use.
*/
const I18nManager = (props, { getState, setState, subscribe }) => {
/**
* A safe utility to resolve a value from a nested object using a string path.
* For example, it turns "buttons.save" into obj['buttons']['save'].
* The optional chaining (`?.`) prevents errors if an intermediate key doesn't exist.
*/
const resolvePath = (obj, path) =>
path.split(".").reduce((current, key) => current?.[key], obj);
/**
* The core translation function. It's conventionally named `t`.
* It finds a translation string for the current language, falls back to English if needed,
* and replaces any dynamic parameters (like {name}).
*/
const t = (key, params = {}) => {
const lang = getState("ui.lang") || "en";
const str =
resolvePath(translations[lang], key) || // 1. Try current language
resolvePath(translations["en"], key) || // 2. Fallback to English
key; // 3. Fallback to the key itself
// This loop replaces placeholders like `{name}` with actual values from `params`.
return Object.entries(params).reduce(
(acc, [p, value]) => acc.replace(new RegExp(`\\{${p}\\}`, 'g'), value),
str
);
};
return {
// The `api` object is the public contract of this service.
// Only the `t` function is exposed to the rest of the application.
api: { t },
// `hooks` are lifecycle methods. `onRegister` runs once when JurisJS initializes this service.
hooks: {
onRegister: () => {
// `subscribe` creates a reactive connection. This callback will run automatically
// *every time* the 'ui.lang' state changes. It's highly efficient.
subscribe("ui.lang", () => {
document.title = t("appTitle");
});
},
},
};
};
Look at this demo :
Here is the full commented application, you copy and paste.
May I insist in the fact that this code does not necessitate any bundler, and runs directly in the browser !!!.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<!-- The document title will be dynamically updated by our JurisJS app. -->
<title>JurisJS I18n Demo</title>
<!-- For this educational demo, we import JurisJS directly from a CDN. -->
<script src="https://unpkg.com/juris@0.88"></script>
<!-- We use a clean, modern font from Google to improve readability. -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Lato:wght@400;700&display=swap" rel="stylesheet">
<style>
/*
* --- Educational CSS Styling ---
* This CSS is designed to create a clean, focused presentation
* for a code demonstration, making the output clear and intuitive.
*/
/* 1. General Body Styling */
body {
font-family: 'Lato', sans-serif;
background-color: #f4f7f9; /* A soft, neutral background reduces eye strain. */
color: #333;
display: flex; /* Flexbox is used to easily center the main container. */
justify-content: center;
align-items: center;
min-height: 100vh;
margin: 0;
}
/* 2. Application Root Container */
#app {
background-color: #ffffff;
border-radius: 12px; /* Soft rounded corners give a modern feel. */
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.08); /* A subtle shadow lifts the card off the page. */
padding: 40px;
width: 100%;
max-width: 600px;
text-align: center;
}
/* 3. Header Styling */
.header h2 {
color: #1a237e; /* A deep blue provides a professional, trustworthy tone. */
font-size: 1.8rem;
}
.header p {
color: #667;
line-height: 1.6; /* Increased line-height improves readability for paragraphs. */
}
/* 4. Main Welcome Message Styling */
h1 {
font-size: 2.5rem;
margin: 20px 0;
/* This transition ensures the text change feels smooth when switching languages. */
transition: all 0.3s ease-in-out;
}
/* 5. Button Styling & User Feedback */
.button-group {
margin-top: 25px;
display: flex;
gap: 15px; /* `gap` is a modern CSS property for spacing flex items. */
justify-content: center;
}
button {
font-family: 'Lato', sans-serif;
font-size: 1rem;
font-weight: 700;
padding: 12px 24px;
border: none;
border-radius: 8px;
cursor: pointer;
/* Transitions on multiple properties provide smooth visual feedback on interaction. */
transition: background-color 0.2s ease-in-out, transform 0.2s ease-in-out;
}
button:hover {
transform: translateY(-2px); /* A subtle "lift" effect on hover. */
}
/* Visual hierarchy: Primary action buttons are styled more prominently. */
.action-buttons button {
background-color: #3f51b5;
color: white;
}
.action-buttons button:hover {
background-color: #303f9f;
}
</style>
</head>
<body>
<!-- This is the mounting point. The entire UI will be generated by JurisJS inside this div. -->
<div id="app"></div>
<script>
/*
* ===================================================================================
* JURISJS APPLICATION LOGIC
* Our application is cleanly structured into four main parts:
* 1. The Service (I18nManager): A headless component that handles all translation logic.
* 2. The Data (Translations): A simple object holding all our translation strings.
* 3. The UI Components: Small, focused components that render parts of our view.
* 4. The Application Instance: The main object that wires everything together.
* ===================================================================================
*/
// --- 1. THE SERVICE (Headless Component) ---
/**
* @name I18nManager
* @description A headless component that provides internationalization (I18n) services.
* It contains no UI itself; its only job is to manage translation logic and expose
* an API (`t`) that other components can use.
*/
const I18nManager = (props, { getState, setState, subscribe }) => {
/**
* A safe utility to resolve a value from a nested object using a string path.
* For example, it turns "buttons.save" into obj['buttons']['save'].
* The optional chaining (`?.`) prevents errors if an intermediate key doesn't exist.
*/
const resolvePath = (obj, path) =>
path.split(".").reduce((current, key) => current?.[key], obj);
/**
* The core translation function. It's conventionally named `t`.
* It finds a translation string for the current language, falls back to English if needed,
* and replaces any dynamic parameters (like {name}).
*/
const t = (key, params = {}) => {
const lang = getState("ui.lang") || "en";
const str =
resolvePath(translations[lang], key) || // 1. Try current language
resolvePath(translations["en"], key) || // 2. Fallback to English
key; // 3. Fallback to the key itself
// This loop replaces placeholders like `{name}` with actual values from `params`.
return Object.entries(params).reduce(
(acc, [p, value]) => acc.replace(new RegExp(`\\{${p}\\}`, 'g'), value),
str
);
};
return {
// The `api` object is the public contract of this service.
// Only the `t` function is exposed to the rest of the application.
api: { t },
// `hooks` are lifecycle methods. `onRegister` runs once when JurisJS initializes this service.
hooks: {
onRegister: () => {
// `subscribe` creates a reactive connection. This callback will run automatically
// *every time* the 'ui.lang' state changes. It's highly efficient.
subscribe("ui.lang", () => {
document.title = t("appTitle");
});
},
},
};
};
// --- 2. THE DATA ---
/**
* @name translations
* @description A simple object containing all UI strings.
* Separating text from logic makes the app easier to maintain and translate.
* In a larger application, this data would likely come from separate JSON files.
*/
const translations = {
en: {
appTitle: 'JurisJS I18n Demo',
demoTitle: 'JurisJS Internationalization (I18n) Demo',
demoDescription: 'This is a live demonstration of a headless component managing language state. Click the buttons below to change the language and see the UI update reactively.',
welcome: 'Welcome, {name}!',
buttons: { save: 'Save', cancel: 'Cancel' }
},
fr: {
appTitle: 'Démo I18n de JurisJS',
demoTitle: 'Démo d\'Internationalisation (I18n) de JurisJS',
demoDescription: 'Ceci est une démonstration en direct d\'un composant sans tête gérant l\'état de la langue. Cliquez sur les boutons ci-dessous pour changer la langue et voir l\'interface se mettre à jour réactivement.',
welcome: 'Bienvenue, {name}!',
buttons: { save: 'Enregistrer', cancel: 'Annuler' }
}
};
// --- 3. THE UI COMPONENTS ---
/**
* @name Header
* @description A simple component responsible for rendering the main title and description.
* Notice how `I18n` is destructured from the context parameter. JurisJS provides this automatically.
*/
const Header = (props, { I18n }) => {
// It returns a JurisJS Virtual DOM object that describes the desired HTML.
return {
div: {
class: "header",
children: [
{ h2: { text: () => I18n.t("demoTitle") } },
{ p: { text: () => I18n.t("demoDescription") } },
],
},
};
};
/**
* @name WelcomeMessage
* @description Renders the main `<h1>` greeting.
*/
const WelcomeMessage = (props, { I18n }) => {
return {
h1: { text: () => I18n.t("welcome", { name: "Paul" }) },
};
};
/**
* @name LanguageSelector
* @description Renders the buttons that allow the user to switch languages.
* The `onclick` handlers directly update the central state.
*/
const LanguageSelector = (props, { setState }) => {
return {
div: {
class: "button-group",
children: [
{ button: { text: "Français", onclick: () => setState("ui.lang", "fr") } },
{ button: { text: "English", onclick: () => setState("ui.lang", "en") } },
],
},
}
}
/**
* @name ActionButtons
* @description Renders the primary action buttons (Save/Cancel).
* This component demonstrates effortless access to the translation service.
*/
const ActionButtons = (props, { I18n }) => {
return {
div: {
class: "button-group action-buttons",
children: [
{ button: { text: () => I18n.t("buttons.save") } },
{ button: { text: () => I18n.t("buttons.cancel") } },
],
},
}
}
// --- 4. THE APPLICATION INSTANCE ---
/**
* This is the main application instance where we assemble all the pieces.
*/
const app = new Juris({
// Tells JurisJS where in the HTML to render the app.
target: "#root",
// The application's "single source of truth." All dynamic data lives here.
states: {
ui: {
lang: "en",
},
},
// This is where we register our `I18nManager` service. We give it the public
// name "I18n", which is how it will be identified on the `context` object.
headlessComponents: {
I18n: { fn: I18nManager, options: { autoInit: true } },
},
// A registry of all the UI components we've defined.
components: {
Header,
WelcomeMessage,
LanguageSelector,
ActionButtons
},
// The declarative blueprint of our UI. It defines the structure and order
// of the components to be rendered.
layout: [
{ Header },
{ WelcomeMessage },
{ LanguageSelector },
{ ActionButtons }
],
});
// This final command kicks everything off, rendering the application to the DOM.
app.render();
</script>
</body>
</html>
Top comments (0)