19 | A blazing-fast and lightweight internationalization (i18n) library for your next web-based project
20 |
21 |
22 |
23 |
24 |
25 |
26 | * Contains a [lit](https://www.npmjs.com/package/lit) directive that automatically updates the translations when the language changes
27 | * Has a simple API that can return a translation for a given key using the dot notation (eg. `get("home.header.title")`)
28 | * Works very well with JSON based translation data-structures
29 | * Can interpolate values into the strings using the {{ key }} syntax out of the box
30 | * Caches the translations for maximum performance
31 | * Has a very small footprint, approximately 800 bytes minified & gzipped (2kb without)
32 | * Extremely customizable, just about everything can be changed (eg. choose your own translations loader, how to interpolate values, empty placeholder and how to look up the strings)
33 | * Check out the playground [here](https://codepen.io/andreasbm/pen/MWWXPNO?editors=1010)
34 |
35 |
36 | [](#table-of-contents)
37 |
38 | ## ➤ Table of Contents
39 |
40 | * [➤ Installation](#-installation)
41 | * [➤ 1. Define the translations](#-1-define-the-translations)
42 | * [➤ 2. Register the translate config](#-2-register-the-translate-config)
43 | * [➤ 3. Set the language](#-3-set-the-language)
44 | * [➤ 4. Get the translations](#-4-get-the-translations)
45 | * [➤ 5. Interpolate values](#-5-interpolate-values)
46 | * [➤ 6. Use the `translate` directive with `lit`](#-6-use-the-translate-directive-with-lit)
47 | * [➤ Wait for strings to be loaded before displaying your app](#-wait-for-strings-to-be-loaded-before-displaying-your-app)
48 | * [➤ Advanced Customisation](#-advanced-customisation)
49 | * [Format text with `IntlMessageFormat`](#format-text-with-intlmessageformat)
50 | * [Use the default translations as keys](#use-the-default-translations-as-keys)
51 | * [➤ Typesafe Translations](#-typesafe-translations)
52 | * [1. Add `resolveJsonModule` to your tsconfig](#1-add-resolvejsonmodule-to-your-tsconfig)
53 | * [2. Use the `typedKeysFactory` function](#2-use-the-typedkeysfactory-function)
54 | * [3. Import the typed functions](#3-import-the-typed-functions)
55 | * [➤ `lit` Directives](#-lit-directives)
56 | * [Re-render a value when the language changes with the `langChanged` directive](#re-render-a-value-when-the-language-changes-with-the-langchanged-directive)
57 | * [Create your own `lit` directives that re-renders a value when the language changes](#create-your-own-lit-directives-that-re-renders-a-value-when-the-language-changes)
58 | * [➤ License](#-license)
59 |
60 |
61 | [](#installation)
62 |
63 | ## ➤ Installation
64 |
65 | ```js
66 | npm i lit-translate
67 | ```
68 |
69 | [](#1-define-the-translations)
70 |
71 | ## ➤ 1. Define the translations
72 |
73 | Create a `.json` file for each language you want to support. Heres an example of how `en.json` could look like.
74 |
75 | ```json
76 | {
77 | "header": {
78 | "title": "Hello",
79 | "subtitle": "World"
80 | },
81 | "cta": {
82 | "awesome": "{{ animals }} are awesome!",
83 | "cats": "Cats"
84 | },
85 | "footer": {
86 | "html": "Bold text"
87 | }
88 | }
89 | ```
90 |
91 |
92 | [](#2-register-the-translate-config)
93 |
94 | ## ➤ 2. Register the translate config
95 |
96 | Use the `registerTranslateConfig` function to register a loader that loads translations based on the selected language. In the example below, a loader is registered that uses the [fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API) to load a `.json` file for the selected language.
97 |
98 | ```typescript
99 | import { registerTranslateConfig } from "lit-translate";
100 |
101 | registerTranslateConfig({
102 | loader: lang => fetch(`${lang}.json`).then(res => res.json())
103 | });
104 | ```
105 |
106 |
107 | [](#3-set-the-language)
108 |
109 | ## ➤ 3. Set the language
110 |
111 | Set the language with the `use` function. When called it will use the registered loader from [step 2](#-2-register-the-translate-config) to load the strings for the selected language.
112 |
113 | ```typescript
114 | import { use } from "lit-translate";
115 |
116 | use("en");
117 | ```
118 |
119 |
120 | [](#4-get-the-translations)
121 |
122 | ## ➤ 4. Get the translations
123 |
124 | Get translations with the `get` function. Give this function a string of keys (separated with `.`) that points to the desired translation in the JSON structure. The example below is based on the translations defined in [step 1](#-1-define-the-translations) and registered in [step 2](#-2-register-the-translate-config).
125 |
126 | ```typescript
127 | import { get } from "lit-translate";
128 |
129 | get("header.title"); // "Hello"
130 | get("header.subtitle"); // "World"
131 | ```
132 |
133 |
134 | [](#5-interpolate-values)
135 |
136 | ## ➤ 5. Interpolate values
137 |
138 | When using the `get` function it is possible to interpolate values (replacing placeholders with content). As default, you can use the `{{ key }}` syntax in your translations and provide an object with values replacing those defined in the translations when using the `get` function. The example below is based on the strings defined in [step 1](#-1-define-the-translations) and registered in [step 2](#-2-register-the-translate-config).
139 |
140 | ```typescript
141 | import { get } from "lit-translate";
142 |
143 | get("cta.awesome", { animals: get("cta.cats") }); // Cats are awesome!
144 | ```
145 |
146 |
147 |
148 | [](#6-use-the-translate-directive-with-lit)
149 |
150 | ## ➤ 6. Use the `translate` directive with `lit`
151 |
152 | If you are using [lit](https://www.npmjs.com/package/lit) you might want to use the `translate` directive. This directive makes sure to automatically update all the translated parts when the `use` function is called with a new language. If your strings contain HTML you can use the `translateUnsafeHTML` directive. The example below is based on the strings defined in [step 1](#-1-define-the-translations) and registered in [step 2](#-2-register-the-translate-config).
153 |
154 | ```typescript
155 | import { translate, translateUnsafeHTML } from "lit-translate";
156 | import { LitElement, html } from "lit";
157 | import { customElement } from "lit/decorators.js";
158 |
159 | @customElement("my-element")
160 | class MyElement extends LitElement {
161 | render () {
162 | html`
163 |
${translate("header.title")}
164 |
${translate("header.subtitle")}
165 | ${translate("cta.awesome", { animals: () => get("cta.cats") })}
166 | ${translateUnsafeHTML("footer.html")}
167 | `;
168 | }
169 | }
170 | ```
171 |
172 |
173 | [](#wait-for-strings-to-be-loaded-before-displaying-your-app)
174 |
175 | ## ➤ Wait for strings to be loaded before displaying your app
176 |
177 | You might want to avoid empty placeholders being shown initially before any of the translation strings have been loaded. This it how you could defer the first render of your app until the strings have been loaded.
178 |
179 | ```typescript
180 | import { use, translate } from "lit-translate";
181 | import { LitElement, html, PropertyValues } from "lit";
182 | import { customElement, state } from "lit/decorators.js";
183 |
184 | @customElement("my-app")
185 | export class MyApp extends LitElement {
186 |
187 | // Defer the first update of the component until the strings has been loaded to avoid empty strings being shown
188 | @state() hasLoadedStrings = false;
189 |
190 | protected shouldUpdate(props: PropertyValues) {
191 | return this.hasLoadedStrings && super.shouldUpdate(props);
192 | }
193 |
194 | // Load the initial language and mark that the strings has been loaded so the component can render.
195 | async connectedCallback() {
196 | super.connectedCallback();
197 |
198 | await use("en");
199 | this.hasLoadedStrings = true;
200 | }
201 |
202 | // Render the component
203 | protected render () {
204 | return html`
205 |
${translate("title")}
206 | `;
207 | }
208 | }
209 | ```
210 |
211 |
212 | [](#advanced-customisation)
213 |
214 | ## ➤ Advanced Customisation
215 |
216 | If you want you can customise just about anything by overwriting the configuration hooks. Below is an example of what you can customise. Try it as a playground [here](https://codepen.io/andreasbm/pen/gOoVGdQ?editors=0010).
217 |
218 | ```typescript
219 | import { registerTranslateConfig, extract, get, use } from "lit-translate";
220 |
221 | registerTranslateConfig({
222 |
223 | // Loads the language by returning a JSON structure for a given language
224 | loader: lang => {
225 | switch (lang) {
226 |
227 | // English strings
228 | case "en":
229 | return {
230 | app: {
231 | title: "This is a title",
232 | description: "This description is {placeholder}!"
233 | },
234 | awesome: "awesome"
235 | };
236 |
237 | // Danish strings
238 | case "da":
239 | return {
240 | app: {
241 | title: "Dette er en titel",
242 | description: "Denne beskrivelse er {placeholder}!"
243 | },
244 | awesome: "fed"
245 | };
246 |
247 | default:
248 | throw new Error(`The language ${lang} is not supported..`);
249 | }
250 | },
251 |
252 | // Interpolate the values using a key syntax.
253 | interpolate: (text, values) => {
254 | for (const [key, value] of Object.entries(extract(values || {}))) {
255 | text = text.replace(new RegExp(`{.*${key}.*}`, `gm`), String(extract(value)));
256 | }
257 |
258 |
259 | return text;
260 | },
261 |
262 | // Returns a string for a given key
263 | lookup: (key, config) => {
264 |
265 | // Split the key in parts (example: hello.world)
266 | const parts = key.split(" -> ");
267 |
268 | // Find the string by traversing through the strings matching the chain of keys
269 | let string = config.strings;
270 |
271 | // Shift through all the parts of the key while matching with the strings.
272 | // Do not continue if the string is not defined or if we have traversed all the key parts
273 | while (string != null && parts.length > 0) {
274 | string = string[parts.shift()];
275 | }
276 |
277 | // Make sure the string is in fact a string!
278 | return string != null ? string.toString() : null;
279 | },
280 |
281 | // Formats empty placeholders (eg. !da.headline.title!) if lookup returns null
282 | empty: (key, config) => `!${config.lang}.${key}!`
283 | });
284 |
285 | use("en").then(() => {
286 | get("app -> description", { placeholder: get("awesome") }); // Will return "This description is awesome"
287 | });
288 | ```
289 |
290 | ### Format text with `IntlMessageFormat`
291 |
292 | [IntlMessageFormat](https://www.npmjs.com/package/intl-messageformat) is a library that formats ICU message strings with number, date, plural, and select placeholders to create localized messages using [ICU placeholders](https://unicode-org.github.io/icu/userguide/format_parse/messages/). This library is a good addition to `lit-translate`. You can add it to the interpolate hook to get the benefits as shown in the following example. Try the example as a playground [here](https://codepen.io/andreasbm/pen/rNpXGPW?editors=0010).
293 |
294 | ```typescript
295 | import { registerTranslateConfig, extract } from "lit-translate";
296 | import { IntlMessageFormat } from "intl-messageformat";
297 |
298 | registerTranslateConfig({
299 | loader: lang => {
300 | switch (lang) {
301 | case "en":
302 | return {
303 | photos: `You have {numPhotos, plural, =0 {no photos.} =1 {one photo.} other {# photos.}}`
304 | };
305 |
306 | case "en":
307 | return {
308 | photos: `Du har {numPhotos, plural, =0 {ingen billeder.} =1 {et billede.} other {# billeder.}}`
309 | };
310 |
311 | default:
312 | throw new Error(`The language ${lang} is not supported..`);
313 | }
314 | },
315 |
316 | // Use the "intl-messageformat" library for formatting.
317 | interpolate: (text, values, config) => {
318 | const msg = new IntlMessageFormat(text, config.lang);
319 | return msg.format(extract(values));
320 | }
321 | });
322 |
323 | use("en").then(() => {
324 | get("photos", {numPhotos: 0}); // Will return "You have no photos"
325 | get("photos", {numPhotos: 1}); // Will return "You have one photo."
326 | get("photos", {numPhotos: 5}); // Will return "You have 5 photos."
327 | });
328 | ```
329 |
330 | ### Use the default translations as keys
331 |
332 | Inspired by [GNU gettext](https://en.wikipedia.org/wiki/Gettext) you can use the default translation as keys. The benefit of doing this is that you will save typing time and reduce code clutter. You can use [xgettext](https://www.gnu.org/software/gettext/manual/html_node/xgettext-Invocation.html) to extract the translatable strings from your code and then use [po2json](https://github.com/mikeedwards/po2json) to turn your `.po` files into `.json` files. The following code shows an example of how you could implement this. Try it as a playground [here](https://codepen.io/andreasbm/pen/RwxXjJX?editors=0010).
333 |
334 | ```typescript
335 | import { registerTranslateConfig, use, get } from "lit-translate";
336 |
337 | registerTranslateConfig({
338 | loader: lang => {
339 | switch (lang) {
340 | case "da":
341 | return {
342 | "The page is being loaded...": "Siden indlæses..."
343 | };
344 | default:
345 | return {};
346 | }
347 | },
348 | lookup: (key, config) => config.strings != null && config.strings[key] != null ? config.strings[key].toString() : key,
349 | empty: key => key,
350 | });
351 |
352 | get("The page is being loaded..."); // Will return "The page is being loaded..."
353 |
354 | use("da").then(() => {
355 | get("The page is being loaded..."); // Will return "Siden indlæses..."
356 | });
357 | ```
358 |
359 | [](#typesafe-translations)
360 |
361 | ## ➤ Typesafe Translations
362 |
363 |
364 |
365 | If you have a lot of translation keys you can quickly lose the overview of your strings. If you use Typescript you can make the keys of your translation keys typesafe - this will also give you autocompletion when you enter the keys. To achieve this you have to do the following:
366 |
367 |
368 | ### 1. Add `resolveJsonModule` to your tsconfig
369 |
370 | Add [resolveJsonModule](https://www.typescriptlang.org/tsconfig#resolveJsonModule) to your `tsconfig` which will allow us to import modules with a `.json` extension.
371 |
372 | ```json
373 | {
374 | ...
375 | "compilerOptions": {
376 | ...
377 | "resolveJsonModule": true
378 | }
379 | }
380 | ```
381 |
382 | ### 2. Use the `typedKeysFactory` function
383 |
384 | Create a file, for example `typed-lit-translate.ts`. Then use the factory function `typedKeysFactory` and provide it with the type of one of your translation files. Use `typeof import(..)` to import the `.json` file and get the type. Provide this type to the factory function, and it will return a version of `get`, `translate` and `translateUnsafeHTML` where the keys are typed. Export these and make sure to import from your `typed-lit-translate.ts` file instead of `lit-translate`.
385 |
386 | ```typescript
387 | // typed-lit-translate.ts
388 | import { typedKeysFactory } from "lit-translate";
389 |
390 | const { get, translate, translateUnsafeHTML } = typedKeysFactory();
391 | export { get, translate, translateUnsafeHTML };
392 | ```
393 |
394 | ### 3. Import the typed functions
395 |
396 | Make sure to import the typed versions of `get`, `translate` and `translateUnsafeHTML` that you have created instead of importing from `lit-translate`.
397 |
398 | ```typescript
399 | import { get } from "typed-lit-translate.ts";
400 |
401 | get("this.key.is.typed");
402 | ```
403 |
404 | [](#lit-directives)
405 |
406 | ## ➤ `lit` Directives
407 |
408 | ### Re-render a value when the language changes with the `langChanged` directive
409 |
410 | Use the `langChanged` directive to re-render a value when the language changes.
411 |
412 | ```typescript
413 | import { langChanged, translateConfig } from "lit-translate";
414 | import { html, LitElement, TemplateResult } from "lit";
415 | import { customElement } from "lit/decorators.js";
416 |
417 | @customElement("my-component")
418 | export class MyComponent extends LitElement {
419 | protected render(): TemplateResult {
420 | return html`
421 |
422 | `;
423 | }
424 | }
425 | ```
426 |
427 | ### Create your own `lit` directives that re-renders a value when the language changes
428 |
429 | Extend the `LangChangedDirectiveBase` base class to create your own directives that re-renders a value when the language changes. Below is an example of a directive that localizes assets paths based on the selected language.
430 |
431 | ```typescript
432 | import { LangChangedDirectiveBase, translateConfig } from "lit-translate";
433 | import { directive } from "lit/directive.js";
434 |
435 | export const localizeAssetPath = directive(class extends LangChangedDirectiveBase {
436 | render (fileName: string, config = translateConfig) {
437 | return this.renderValue(() => `localized-assets/${config.lang || "en"}/${fileName}`);
438 | }
439 | });
440 | ```
441 |
442 |
443 | [](#license)
444 |
445 | ## ➤ License
446 |
447 | Licensed under [MIT](https://opensource.org/licenses/MIT).
--------------------------------------------------------------------------------
/blueprint.json:
--------------------------------------------------------------------------------
1 | {
2 | "line": "rainbow",
3 | "toc": true,
4 | "placeholder": ["[[", "]]"],
5 | "ids": {
6 | "github": "andreasbm/lit-translate",
7 | "npm": "lit-translate",
8 | "webcomponents": "lit-translate"
9 | },
10 | "badges": [
11 | {
12 | "text": "Awesome",
13 | "url": "https://github.com/web-padawan/awesome-lit-html",
14 | "img": "https://awesome.re/badge.svg"
15 | }
16 | ],
17 | "bullets": [
18 | "Contains a [lit](https://www.npmjs.com/package/lit) directive that automatically updates the translations when the language changes",
19 | "Has a simple API that can return a translation for a given key using the dot notation (eg. `get(\"home.header.title\")`)",
20 | "Works very well with JSON based translation data-structures",
21 | "Can interpolate values into the strings using the {{ key }} syntax out of the box",
22 | "Caches the translations for maximum performance",
23 | "Has a very small footprint, approximately 800 bytes minified & gzipped (2kb without)",
24 | "Extremely customizable, just about everything can be changed (eg. choose your own translations loader, how to interpolate values, empty placeholder and how to look up the strings)",
25 | "Check out the playground [here](https://codepen.io/andreasbm/pen/MWWXPNO?editors=1010)"
26 | ]
27 | }
--------------------------------------------------------------------------------
/blueprint.md:
--------------------------------------------------------------------------------
1 | [[ template:title ]]
2 |
3 |
8 |
9 | [[ template:badges ]]
10 | [[ template:description ]]
11 |
12 | [[ bullets ]]
13 |
14 | [[ template:toc ]]
15 |
16 | [[ load:readme/installation.md ]]
17 | [[ load:readme/1-define.md ]]
18 | [[ load:readme/2-register.md ]]
19 | [[ load:readme/3-set-language.md ]]
20 | [[ load:readme/4-get-translations.md ]]
21 | [[ load:readme/5-interpolate.md ]]
22 | [[ load:readme/6-lit.md ]]
23 | [[ load:readme/wait.md ]]
24 | [[ load:readme/customise.md ]]
25 | [[ load:readme/typesafe.md ]]
26 | [[ load:readme/directives.md ]]
27 |
28 | [[ template:license ]]
--------------------------------------------------------------------------------
/example.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/andreasbm/lit-translate/2ec157a9ab83e38b2d2429426c087bdb03802fdc/example.gif
--------------------------------------------------------------------------------
/karma.conf.js:
--------------------------------------------------------------------------------
1 | const {defaultResolvePlugins, defaultKarmaConfig} = require("@appnest/web-config");
2 |
3 | module.exports = (config) => {
4 | config.set({
5 | ...defaultKarmaConfig({
6 | rollupPlugins: defaultResolvePlugins()
7 | }),
8 | basePath: "src/test",
9 | logLevel: config.LOG_INFO
10 | });
11 | };
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "lit-translate",
3 | "version": "2.0.1",
4 | "license": "MIT",
5 | "module": "index.js",
6 | "author": "Appnest",
7 | "description": "A blazing-fast and lightweight internationalization (i18n) library for your next web-based project",
8 | "bugs": {
9 | "url": "https://github.com/andreasbm/lit-translate/issues"
10 | },
11 | "homepage": "https://github.com/andreasbm/lit-translate#readme",
12 | "repository": {
13 | "type": "git",
14 | "url": "git+https://github.com/andreasbm/lit-translate.git"
15 | },
16 | "keywords": [
17 | "lit-html",
18 | "lit-element",
19 | "lit",
20 | "custom",
21 | "elements",
22 | "web",
23 | "component",
24 | "custom element",
25 | "web component",
26 | "util",
27 | "decorators",
28 | "directives",
29 | "translate",
30 | "localisation",
31 | "localization"
32 | ],
33 | "main": "index.js",
34 | "types": "index.d.ts",
35 | "scripts": {
36 | "start": "npm run s",
37 | "ncu": "ncu -u -a && npm update && npm install",
38 | "test": "karma start karma.conf.js",
39 | "b:lib": "node pre-build.js && tsc -p tsconfig.build.json",
40 | "b:demo:dev": "rollup -c --environment NODE_ENV:dev",
41 | "b:demo:prod": "rollup -c --environment NODE_ENV:prod",
42 | "s:dev": "rollup -c --watch --environment NODE_ENV:dev",
43 | "s:prod": "rollup -c --watch --environment NODE_ENV:prod",
44 | "s": "npm run s:dev",
45 | "readme": "node node_modules/.bin/readme generate",
46 | "postversion": "npm run readme && npm run b:lib",
47 | "publish:patch": "np patch --contents=dist --no-cleanup",
48 | "publish:minor": "np minor --contents=dist --no-cleanup",
49 | "publish:major": "np major --contents=dist --no-cleanup"
50 | },
51 | "dependencies": {
52 | "lit": "^2.2.2"
53 | },
54 | "devDependencies": {
55 | "@appnest/readme": "^1.2.7",
56 | "@appnest/web-config": "0.5.4",
57 | "lit": "^2.2.2"
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/pre-build.js:
--------------------------------------------------------------------------------
1 | const rimraf = require("rimraf");
2 | const path = require("path");
3 | const fs = require("fs-extra");
4 | const outLib = "dist";
5 |
6 | // TODO: Run "tsc -p tsconfig.build.json" from this script and rename it to "build".
7 |
8 | async function preBuild () {
9 | await cleanLib();
10 | copySync("./package.json", `./${outLib}/package.json`);
11 | copySync("./README.md", `./${outLib}/README.md`);
12 | }
13 |
14 | function cleanLib () {
15 | return new Promise(res => {
16 | rimraf(outLib, res);
17 | });
18 | }
19 |
20 | function copySync (src, dest) {
21 | fs.copySync(path.resolve(__dirname, src), path.resolve(__dirname, dest));
22 | }
23 |
24 | preBuild().then(_ => {
25 | console.log(">> Prebuild completed");
26 | });
27 |
--------------------------------------------------------------------------------
/readme/1-define.md:
--------------------------------------------------------------------------------
1 | ## 1. Define the translations
2 |
3 | Create a `.json` file for each language you want to support. Heres an example of how `en.json` could look like.
4 |
5 | ```json
6 | {
7 | "header": {
8 | "title": "Hello",
9 | "subtitle": "World"
10 | },
11 | "cta": {
12 | "awesome": "{{ animals }} are awesome!",
13 | "cats": "Cats"
14 | },
15 | "footer": {
16 | "html": "Bold text"
17 | }
18 | }
19 | ```
20 |
--------------------------------------------------------------------------------
/readme/2-register.md:
--------------------------------------------------------------------------------
1 | ## 2. Register the translate config
2 |
3 | Use the `registerTranslateConfig` function to register a loader that loads translations based on the selected language. In the example below, a loader is registered that uses the [fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API) to load a `.json` file for the selected language.
4 |
5 | ```typescript
6 | import { registerTranslateConfig } from "lit-translate";
7 |
8 | registerTranslateConfig({
9 | loader: lang => fetch(`${lang}.json`).then(res => res.json())
10 | });
11 | ```
12 |
--------------------------------------------------------------------------------
/readme/3-set-language.md:
--------------------------------------------------------------------------------
1 | ## 3. Set the language
2 |
3 | Set the language with the `use` function. When called it will use the registered loader from [step 2](#-2-register-the-translate-config) to load the strings for the selected language.
4 |
5 | ```typescript
6 | import { use } from "lit-translate";
7 |
8 | use("en");
9 | ```
10 |
--------------------------------------------------------------------------------
/readme/4-get-translations.md:
--------------------------------------------------------------------------------
1 | ## 4. Get the translations
2 |
3 | Get translations with the `get` function. Give this function a string of keys (separated with `.`) that points to the desired translation in the JSON structure. The example below is based on the translations defined in [step 1](#-1-define-the-translations) and registered in [step 2](#-2-register-the-translate-config).
4 |
5 | ```typescript
6 | import { get } from "lit-translate";
7 |
8 | get("header.title"); // "Hello"
9 | get("header.subtitle"); // "World"
10 | ```
11 |
--------------------------------------------------------------------------------
/readme/5-interpolate.md:
--------------------------------------------------------------------------------
1 | ## 5. Interpolate values
2 |
3 | When using the `get` function it is possible to interpolate values (replacing placeholders with content). As default, you can use the `{{ key }}` syntax in your translations and provide an object with values replacing those defined in the translations when using the `get` function. The example below is based on the strings defined in [step 1](#-1-define-the-translations) and registered in [step 2](#-2-register-the-translate-config).
4 |
5 | ```typescript
6 | import { get } from "lit-translate";
7 |
8 | get("cta.awesome", { animals: get("cta.cats") }); // Cats are awesome!
9 | ```
10 |
11 |
--------------------------------------------------------------------------------
/readme/6-lit.md:
--------------------------------------------------------------------------------
1 | ## 6. Use the `translate` directive with `lit`
2 |
3 | If you are using [lit](https://www.npmjs.com/package/lit) you might want to use the `translate` directive. This directive makes sure to automatically update all the translated parts when the `use` function is called with a new language. If your strings contain HTML you can use the `translateUnsafeHTML` directive. The example below is based on the strings defined in [step 1](#-1-define-the-translations) and registered in [step 2](#-2-register-the-translate-config).
4 |
5 | ```typescript
6 | import { translate, translateUnsafeHTML } from "lit-translate";
7 | import { LitElement, html } from "lit";
8 | import { customElement } from "lit/decorators.js";
9 |
10 | @customElement("my-element")
11 | class MyElement extends LitElement {
12 | render () {
13 | html`
14 |
${translate("header.title")}
15 |
${translate("header.subtitle")}
16 | ${translate("cta.awesome", { animals: () => get("cta.cats") })}
17 | ${translateUnsafeHTML("footer.html")}
18 | `;
19 | }
20 | }
21 | ```
22 |
--------------------------------------------------------------------------------
/readme/async.md:
--------------------------------------------------------------------------------
1 | ## Asynchronous and Encapsulated Translations
2 |
3 | If you have a lot of strings it might not make sense to load them all at once. In `lit-translate` you can have as many translation configs as you want to. As shown in the example below, the trick to encapsulating the translations and loading them asynchronously is to create a new translation config and use it instead of the global `translateConfig`. We then make sure to provide it to the library functions (such as `use`, `get` and `translate`) and keep the selected language in sync with the global one by listening for the `langChanged` event.
4 |
5 | ```typescript
6 | import { ITranslateConfig, listenForLangChanged, translateConfig, use, get } from "lit-translate";
7 |
8 | // Create a new translation config
9 | const asyncTranslateConfig: ITranslateConfig = {
10 | ...translateConfig,
11 | loader: lang => fetch(`my-component.${lang}.json`).then(res => res.json()),
12 | empty: () => ""
13 | }
14 |
15 | // Initially set the language of asyncTranslateConfig to match the language of translateConfig.
16 | // When calling the use function, make sure to provide asyncTranslateConfig.
17 | if (translateConfig.lang != null) {
18 | use(translateConfig.lang, asyncTranslateConfig);
19 | }
20 |
21 | // Whenever the language of translateConfig changes also update the language of the asyncTranslateConfig to load the strings.
22 | listenForLangChanged(({lang}) => {
23 | if (asyncTranslateConfig.lang !== lang) {
24 | use(lang, asyncTranslateConfig);
25 | }
26 | });
27 |
28 | // When getting translations, provide asyncTranslateConfig to the library functions
29 | get("title", undefined, asyncTranslateConfig);
30 | ```
--------------------------------------------------------------------------------
/readme/customise.md:
--------------------------------------------------------------------------------
1 | ## Advanced Customisation
2 |
3 | If you want you can customise just about anything by overwriting the configuration hooks. Below is an example of what you can customise. Try it as a playground [here](https://codepen.io/andreasbm/pen/gOoVGdQ?editors=0010).
4 |
5 | ```typescript
6 | import { registerTranslateConfig, extract, get, use } from "lit-translate";
7 |
8 | registerTranslateConfig({
9 |
10 | // Loads the language by returning a JSON structure for a given language
11 | loader: lang => {
12 | switch (lang) {
13 |
14 | // English strings
15 | case "en":
16 | return {
17 | app: {
18 | title: "This is a title",
19 | description: "This description is {placeholder}!"
20 | },
21 | awesome: "awesome"
22 | };
23 |
24 | // Danish strings
25 | case "da":
26 | return {
27 | app: {
28 | title: "Dette er en titel",
29 | description: "Denne beskrivelse er {placeholder}!"
30 | },
31 | awesome: "fed"
32 | };
33 |
34 | default:
35 | throw new Error(`The language ${lang} is not supported..`);
36 | }
37 | },
38 |
39 | // Interpolate the values using a [[ key ]] syntax.
40 | interpolate: (text, values) => {
41 | for (const [key, value] of Object.entries(extract(values || {}))) {
42 | text = text.replace(new RegExp(`{.*${key}.*}`, `gm`), String(extract(value)));
43 | }
44 |
45 |
46 | return text;
47 | },
48 |
49 | // Returns a string for a given key
50 | lookup: (key, config) => {
51 |
52 | // Split the key in parts (example: hello.world)
53 | const parts = key.split(" -> ");
54 |
55 | // Find the string by traversing through the strings matching the chain of keys
56 | let string = config.strings;
57 |
58 | // Shift through all the parts of the key while matching with the strings.
59 | // Do not continue if the string is not defined or if we have traversed all the key parts
60 | while (string != null && parts.length > 0) {
61 | string = string[parts.shift()];
62 | }
63 |
64 | // Make sure the string is in fact a string!
65 | return string != null ? string.toString() : null;
66 | },
67 |
68 | // Formats empty placeholders (eg. !da.headline.title!) if lookup returns null
69 | empty: (key, config) => `!${config.lang}.${key}!`
70 | });
71 |
72 | use("en").then(() => {
73 | get("app -> description", { placeholder: get("awesome") }); // Will return "This description is awesome"
74 | });
75 | ```
76 |
77 | ### Format text with `IntlMessageFormat`
78 |
79 | [IntlMessageFormat](https://www.npmjs.com/package/intl-messageformat) is a library that formats ICU message strings with number, date, plural, and select placeholders to create localized messages using [ICU placeholders](https://unicode-org.github.io/icu/userguide/format_parse/messages/). This library is a good addition to `lit-translate`. You can add it to the interpolate hook to get the benefits as shown in the following example. Try the example as a playground [here](https://codepen.io/andreasbm/pen/rNpXGPW?editors=0010).
80 |
81 | ```typescript
82 | import { registerTranslateConfig, extract } from "lit-translate";
83 | import { IntlMessageFormat } from "intl-messageformat";
84 |
85 | registerTranslateConfig({
86 | loader: lang => {
87 | switch (lang) {
88 | case "en":
89 | return {
90 | photos: `You have {numPhotos, plural, =0 {no photos.} =1 {one photo.} other {# photos.}}`
91 | };
92 |
93 | case "en":
94 | return {
95 | photos: `Du har {numPhotos, plural, =0 {ingen billeder.} =1 {et billede.} other {# billeder.}}`
96 | };
97 |
98 | default:
99 | throw new Error(`The language ${lang} is not supported..`);
100 | }
101 | },
102 |
103 | // Use the "intl-messageformat" library for formatting.
104 | interpolate: (text, values, config) => {
105 | const msg = new IntlMessageFormat(text, config.lang);
106 | return msg.format(extract(values));
107 | }
108 | });
109 |
110 | use("en").then(() => {
111 | get("photos", {numPhotos: 0}); // Will return "You have no photos"
112 | get("photos", {numPhotos: 1}); // Will return "You have one photo."
113 | get("photos", {numPhotos: 5}); // Will return "You have 5 photos."
114 | });
115 | ```
116 |
117 | ### Use the default translations as keys
118 |
119 | Inspired by [GNU gettext](https://en.wikipedia.org/wiki/Gettext) you can use the default translation as keys. The benefit of doing this is that you will save typing time and reduce code clutter. You can use [xgettext](https://www.gnu.org/software/gettext/manual/html_node/xgettext-Invocation.html) to extract the translatable strings from your code and then use [po2json](https://github.com/mikeedwards/po2json) to turn your `.po` files into `.json` files. The following code shows an example of how you could implement this. Try it as a playground [here](https://codepen.io/andreasbm/pen/RwxXjJX?editors=0010).
120 |
121 | ```typescript
122 | import { registerTranslateConfig, use, get } from "lit-translate";
123 |
124 | registerTranslateConfig({
125 | loader: lang => {
126 | switch (lang) {
127 | case "da":
128 | return {
129 | "The page is being loaded...": "Siden indlæses..."
130 | };
131 | default:
132 | return {};
133 | }
134 | },
135 | lookup: (key, config) => config.strings != null && config.strings[key] != null ? config.strings[key].toString() : key,
136 | empty: key => key,
137 | });
138 |
139 | get("The page is being loaded..."); // Will return "The page is being loaded..."
140 |
141 | use("da").then(() => {
142 | get("The page is being loaded..."); // Will return "Siden indlæses..."
143 | });
144 | ```
--------------------------------------------------------------------------------
/readme/directives.md:
--------------------------------------------------------------------------------
1 | ## `lit` Directives
2 |
3 | ### Re-render a value when the language changes with the `langChanged` directive
4 |
5 | Use the `langChanged` directive to re-render a value when the language changes.
6 |
7 | ```typescript
8 | import { langChanged, translateConfig } from "lit-translate";
9 | import { html, LitElement, TemplateResult } from "lit";
10 | import { customElement } from "lit/decorators.js";
11 |
12 | @customElement("my-component")
13 | export class MyComponent extends LitElement {
14 | protected render(): TemplateResult {
15 | return html`
16 |
17 | `;
18 | }
19 | }
20 | ```
21 |
22 | ### Create your own `lit` directives that re-renders a value when the language changes
23 |
24 | Extend the `LangChangedDirectiveBase` base class to create your own directives that re-renders a value when the language changes. Below is an example of a directive that localizes assets paths based on the selected language.
25 |
26 | ```typescript
27 | import { LangChangedDirectiveBase, translateConfig } from "lit-translate";
28 | import { directive } from "lit/directive.js";
29 |
30 | export const localizeAssetPath = directive(class extends LangChangedDirectiveBase {
31 | render (fileName: string, config = translateConfig) {
32 | return this.renderValue(() => `localized-assets/${config.lang || "en"}/${fileName}`);
33 | }
34 | });
35 | ```
--------------------------------------------------------------------------------
/readme/installation.md:
--------------------------------------------------------------------------------
1 | ## Installation
2 |
3 | ```js
4 | npm i [[ ids.npm ]]
5 | ```
--------------------------------------------------------------------------------
/readme/typesafe.md:
--------------------------------------------------------------------------------
1 | ## Typesafe Translations
2 |
3 |
4 |
5 | If you have a lot of translation keys you can quickly lose the overview of your strings. If you use Typescript you can make the keys of your translation keys typesafe - this will also give you autocompletion when you enter the keys. To achieve this you have to do the following:
6 |
7 |
8 | ### 1. Add `resolveJsonModule` to your tsconfig
9 |
10 | Add [resolveJsonModule](https://www.typescriptlang.org/tsconfig#resolveJsonModule) to your `tsconfig` which will allow us to import modules with a `.json` extension.
11 |
12 | ```json
13 | {
14 | ...
15 | "compilerOptions": {
16 | ...
17 | "resolveJsonModule": true
18 | }
19 | }
20 | ```
21 |
22 | ### 2. Use the `typedKeysFactory` function
23 |
24 | Create a file, for example `typed-lit-translate.ts`. Then use the factory function `typedKeysFactory` and provide it with the type of one of your translation files. Use `typeof import(..)` to import the `.json` file and get the type. Provide this type to the factory function, and it will return a version of `get`, `translate` and `translateUnsafeHTML` where the keys are typed. Export these and make sure to import from your `typed-lit-translate.ts` file instead of `lit-translate`.
25 |
26 | ```typescript
27 | // typed-lit-translate.ts
28 | import { typedKeysFactory } from "lit-translate";
29 |
30 | const { get, translate, translateUnsafeHTML } = typedKeysFactory();
31 | export { get, translate, translateUnsafeHTML };
32 | ```
33 |
34 | ### 3. Import the typed functions
35 |
36 | Make sure to import the typed versions of `get`, `translate` and `translateUnsafeHTML` that you have created instead of importing from `lit-translate`.
37 |
38 | ```typescript
39 | import { get } from "typed-lit-translate.ts";
40 |
41 | get("this.key.is.typed");
42 | ```
--------------------------------------------------------------------------------
/readme/wait.md:
--------------------------------------------------------------------------------
1 | ## Wait for strings to be loaded before displaying your app
2 |
3 | You might want to avoid empty placeholders being shown initially before any of the translation strings have been loaded. This it how you could defer the first render of your app until the strings have been loaded.
4 |
5 | ```typescript
6 | import { use, translate } from "lit-translate";
7 | import { LitElement, html, PropertyValues } from "lit";
8 | import { customElement, state } from "lit/decorators.js";
9 |
10 | @customElement("my-app")
11 | export class MyApp extends LitElement {
12 |
13 | // Defer the first update of the component until the strings has been loaded to avoid empty strings being shown
14 | @state() hasLoadedStrings = false;
15 |
16 | protected shouldUpdate(props: PropertyValues) {
17 | return this.hasLoadedStrings && super.shouldUpdate(props);
18 | }
19 |
20 | // Load the initial language and mark that the strings has been loaded so the component can render.
21 | async connectedCallback() {
22 | super.connectedCallback();
23 |
24 | await use("en");
25 | this.hasLoadedStrings = true;
26 | }
27 |
28 | // Render the component
29 | protected render () {
30 | return html`
31 |