50 | );
51 | };
52 |
53 | export const pageQuery = graphql`
54 | query($slug: String!) {
55 | markdownRemark(frontmatter: { slug: { eq: $slug } }) {
56 | html
57 | frontmatter {
58 | title
59 | description
60 | prev_link
61 | next_link
62 | }
63 | }
64 | }
65 | `;
66 |
67 | export default Layout;
68 |
--------------------------------------------------------------------------------
/docs/src/pages/quickstart.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Quickstart
3 | description: Get up and running with Simple Translator in no more than 10 minutes.
4 | slug: /quickstart/
5 | ---
6 |
7 | This quickstart guide will show you the basic usage of _Simple Translator_. If you want to learn more details, head over to the [tutorial](/tutorial/).
8 |
9 | ## 1. Installation
10 |
11 | Simple Translator can be installed from npm:
12 |
13 | ```bash
14 | # npm
15 | npm install @andreasremdt/simple-translator
16 |
17 | #yarn
18 | yarn add @andreasremdt/simple-translator
19 | ```
20 |
21 | If you don't want to install a dependency and prefer to directly load _Simple Translator_ into the browser, you can use unpkg:
22 |
23 | ```html
24 |
28 | ```
29 |
30 | ## 2. Import & Initialization
31 |
32 | Import the `Translator` class into your JavaScript file. Depending on your setup, you can either use ESM or CommonJS.
33 |
34 | ```js
35 | // ESM
36 | import Translator from "@andreasremdt/simple-translator";
37 |
38 | // CommonJS
39 | var Translator = require("@andreasremdt/simple-translator");
40 | ```
41 |
42 | If you loaded the library via unpkg, you can skip this step, as the `Translator` class will be available globally.
43 |
44 | Initialize the `Translator` class and provide some (optional) options to configure its behavior. You don't need to pass any configuration, the default options will be used instead.
45 |
46 | ```js
47 | var translator = new Translator({
48 | ...options,
49 | });
50 | ```
51 |
52 | ## 3. Preparing the HTML
53 |
54 | Add `data-i18n` attributes to all HTML elements that you want to translate.
55 |
56 | ```html
57 |
58 | ```
59 |
60 | This will replace the `textContent` of the paragraph with your translation, coming from the translation resource. You can set `data-i18n` to all HTML elements that can have `textContent`.
61 |
62 | ## 4. Translating the HTML
63 |
64 | Finally, you can use the API to add languages and translate the HTML page.
65 |
66 | ```js
67 | var germanTranslation = {
68 | header: {
69 | title: "Eine Überschrift",
70 | },
71 | };
72 |
73 | translator.add("de", germanTranslation).translatePageTo("de");
74 | ```
75 |
76 | You can register as many languages as you want. Keep in mind that the JSON structure in each corresponding language file should be consistent, otherwise, things might break.
77 |
--------------------------------------------------------------------------------
/docs/src/styles/prismjs.css:
--------------------------------------------------------------------------------
1 | /**
2 | * GHColors theme by Avi Aryan (http://aviaryan.in)
3 | * Inspired by Github syntax coloring
4 | */
5 |
6 | code[class*='language-'],
7 | pre[class*='language-'] {
8 | color: #393a34;
9 | font-family: 'IBM Plex Mono', 'Consolas', 'Bitstream Vera Sans Mono',
10 | 'Courier New', Courier, monospace;
11 | direction: ltr;
12 | text-align: left;
13 | white-space: pre-wrap;
14 | word-spacing: normal;
15 | word-break: normal;
16 | font-size: 14px;
17 | tab-size: 2;
18 | hyphens: none;
19 | }
20 |
21 | pre[class*='language-']::-moz-selection,
22 | pre[class*='language-'] ::-moz-selection,
23 | code[class*='language-']::-moz-selection,
24 | code[class*='language-'] ::-moz-selection {
25 | background: #b3d4fc;
26 | }
27 |
28 | pre[class*='language-']::selection,
29 | pre[class*='language-'] ::selection,
30 | code[class*='language-']::selection,
31 | code[class*='language-'] ::selection {
32 | background: #b3d4fc;
33 | }
34 |
35 | /* Code blocks */
36 | pre[class*='language-'] {
37 | padding: 1.5rem;
38 | margin: 1rem 0;
39 | overflow: auto;
40 | background-color: #f9fafb;
41 | border-radius: 4px;
42 | }
43 |
44 | /* Inline code */
45 | :not(pre) > code[class*='language-'] {
46 | padding: 1px 0.4rem;
47 | background-color: #f9fafb;
48 | }
49 |
50 | .token.comment,
51 | .token.prolog,
52 | .token.doctype,
53 | .token.cdata {
54 | color: #999988;
55 | font-style: italic;
56 | }
57 |
58 | .token.namespace {
59 | opacity: 0.7;
60 | }
61 |
62 | .token.string,
63 | .token.attr-value {
64 | color: #e3116c;
65 | }
66 |
67 | .token.punctuation,
68 | .token.operator {
69 | color: #393a34; /* no highlight */
70 | }
71 |
72 | .token.entity,
73 | .token.url,
74 | .token.symbol,
75 | .token.number,
76 | .token.boolean,
77 | .token.variable,
78 | .token.constant,
79 | .token.property,
80 | .token.regex,
81 | .token.inserted {
82 | color: #36acaa;
83 | }
84 |
85 | .token.atrule,
86 | .token.keyword,
87 | .token.attr-name,
88 | .language-autohotkey .token.selector {
89 | color: #00a4db;
90 | }
91 |
92 | .token.function,
93 | .token.deleted,
94 | .language-autohotkey .token.tag {
95 | color: #9a050f;
96 | }
97 |
98 | .token.tag,
99 | .token.selector,
100 | .language-autohotkey .token.keyword {
101 | color: #00009f;
102 | }
103 |
104 | .token.important,
105 | .token.function,
106 | .token.bold {
107 | font-weight: bold;
108 | }
109 |
110 | .token.italic {
111 | font-style: italic;
112 | }
113 |
--------------------------------------------------------------------------------
/src/utils.js:
--------------------------------------------------------------------------------
1 | const CONSOLE_MESSAGES = {
2 | INVALID_PARAM_LANGUAGE: (param) =>
3 | `Invalid parameter for \`language\` provided. Expected a string, but got ${typeof param}.`,
4 | INVALID_PARAM_JSON: (param) =>
5 | `Invalid parameter for \`json\` provided. Expected an object, but got ${typeof param}.`,
6 | EMPTY_PARAM_LANGUAGE: () =>
7 | `The parameter for \`language\` can't be an empty string.`,
8 | EMPTY_PARAM_JSON: () =>
9 | `The parameter for \`json\` must have at least one key/value pair.`,
10 | INVALID_PARAM_KEY: (param) =>
11 | `Invalid parameter for \`key\` provided. Expected a string, but got ${typeof param}.`,
12 | NO_LANGUAGE_REGISTERED: (language) =>
13 | `No translation for language "${language}" has been added, yet. Make sure to register that language using the \`.add()\` method first.`,
14 | TRANSLATION_NOT_FOUND: (key, language) =>
15 | `No translation found for key "${key}" in language "${language}". Is there a key/value in your translation file?`,
16 | INVALID_PARAMETER_SOURCES: (param) =>
17 | `Invalid parameter for \`sources\` provided. Expected either a string or an array, but got ${typeof param}.`,
18 | FETCH_ERROR: (response) =>
19 | `Could not fetch "${response.url}": ${response.status} (${response.statusText})`,
20 | INVALID_ENVIRONMENT: () =>
21 | `You are trying to execute the method \`translatePageTo()\`, which is only available in the browser. Your environment is most likely Node.js`,
22 | MODULE_NOT_FOUND: (message) => message,
23 | MISMATCHING_ATTRIBUTES: (keys, attributes, element) =>
24 | `The attributes "data-i18n" and "data-i18n-attr" must contain the same number of keys.
25 |
26 | Values in \`data-i18n\`: (${keys.length}) \`${keys.join(' ')}\`
27 | Values in \`data-i18n-attr\`: (${attributes.length}) \`${attributes.join(' ')}\`
28 |
29 | The HTML element is:
30 | ${element.outerHTML}`,
31 | INVALID_OPTIONS: (param) =>
32 | `Invalid config passed to the \`Translator\` constructor. Expected an object, but got ${typeof param}. Using default config instead.`,
33 | };
34 |
35 | /**
36 | *
37 | * @param {Boolean} isEnabled
38 | * @return {Function}
39 | */
40 | export function logger(isEnabled) {
41 | return function log(code, ...args) {
42 | if (isEnabled) {
43 | try {
44 | const message = CONSOLE_MESSAGES[code];
45 | throw new TypeError(message ? message(...args) : 'Unhandled Error');
46 | } catch (ex) {
47 | const line = ex.stack.split(/\n/g)[1];
48 | const [method, filepath] = line.split(/@/);
49 |
50 | console.error(`${ex.message}
51 |
52 | This error happened in the method \`${method}\` from: \`${filepath}\`.
53 |
54 | If you don't want to see these error messages, turn off debugging by passing \`{ debug: false }\` to the constructor.
55 |
56 | Error code: ${code}
57 |
58 | Check out the documentation for more details about the API:
59 | https://github.com/andreasremdt/simple-translator#usage
60 | `);
61 | }
62 | }
63 | };
64 | }
65 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing
2 |
3 | Thanks for being willing to contribute!
4 |
5 | Is this **your first time** contributing to a different project? You might be interested in learning more about the workflow in [this free course](https://egghead.io/courses/how-to-contribute-to-an-open-source-project-on-github).
6 |
7 | ## Project setup
8 |
9 | 1. Fork and clone the repo
10 | 2. Run `npm install` to install dependencies
11 | 3. Run `npm run validate` to validate the installation
12 | 4. Create a branch for your PR with `git checkout -b pr/your-branch-name`
13 |
14 | If you want to transpile and build the project, run `npm run build` (minified, production build) or `npm run dev` to compile and watch for file changes during development.
15 |
16 | > Tip: Keep your `master` branch pointing at the original repository and make
17 | > pull requests from branches on your fork. To do this, run:
18 | >
19 | > ```
20 | > git remote add upstream https://github.com/andreasremdt/simple-translator.git
21 | > git fetch upstream
22 | > git branch --set-upstream-to=upstream/master master
23 | > ```
24 | >
25 | > This will add the original repository as a "remote" called "upstream," Then
26 | > fetch the git information from that remote, then set your local `master`
27 | > branch to use the upstream master branch whenever you run `git pull`. Then you
28 | > can make all of your pull request branches based on this `master` branch.
29 | > Whenever you want to update your version of `master`, do a regular `git pull`.
30 |
31 | ## Committing and pushing changes
32 |
33 | Please make sure to run the tests before you commit your changes. You can run `npm run validate` which will format, test, and lint the code. You won't be able to commit without all of these 3 passing.
34 |
35 | This project follows the [Karma Convention](https://karma-runner.github.io/4.0/dev/git-commit-msg.html).
36 |
37 | ### Short form (only subject line)
38 |
39 | ```
40 | ():
41 | ```
42 |
43 | ### Long form (with body)
44 |
45 | ```
46 | ():
47 |
48 |
49 | ```
50 |
51 | #### ``
52 |
53 | Allowed `` values:
54 |
55 | - **feat** (new feature)
56 | - **fix** (bug fix)
57 | - **docs** (changes to documentation)
58 | - **style** (formatting, missing semi colons, etc; no code change)
59 | - **refactor** (refactoring production code)
60 | - **test** (adding missing tests, refactoring tests; no production code change)
61 | - **chore** (updating rollup etc; no production code change)
62 |
63 | #### ``
64 |
65 | One word, maxim two connected by dash, describing the area the changes affect. Example values:
66 |
67 | - build
68 | - translator
69 | - utils
70 | - etc.
71 |
72 | #### ``
73 |
74 | - Use imperative, present tense: _change_ not _changed_ nor _changes_ or
75 | _changing_
76 | - Do not capitalize first letter
77 | - Do not append dot (.) at the end
78 |
79 | ## Help Needed
80 |
81 | Please checkout the [the open issues](https://github.com/andreasremdt/simple-translator/issues).
82 |
83 | Also, please watch the repo and respond to questions/bug reports/feature
84 | requests. Thanks!
85 |
--------------------------------------------------------------------------------
/tests/translator.node.test.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @jest-environment node
3 | */
4 |
5 | import Translator from '../src/translator.js';
6 |
7 | describe('constructor()', () => {
8 | let translator;
9 |
10 | it('creates a global helper', () => {
11 | translator = new Translator();
12 |
13 | expect(global.__).toBeDefined();
14 |
15 | translator = new Translator({ registerGlobally: 't' });
16 |
17 | expect(global.t).toBeDefined();
18 |
19 | delete global.__;
20 | delete global.t;
21 | });
22 |
23 | it('should not create a global helper when turned off', () => {
24 | translator = new Translator({ registerGlobally: false });
25 |
26 | expect(global.__).toBeUndefined();
27 | });
28 |
29 | it('should not try to detect the language on node', () => {
30 | const spy = jest.spyOn(Translator.prototype, '_detectLanguage');
31 | translator = new Translator();
32 |
33 | expect(spy).not.toHaveBeenCalled();
34 | expect(translator.config.defaultLanguage).toBe('en');
35 | });
36 | });
37 |
38 | describe('translatePageTo()', () => {
39 | let translator;
40 | let consoleSpy;
41 |
42 | beforeEach(() => {
43 | translator = new Translator({ debug: true });
44 | translator.add('de', { title: 'Deutscher Titel' });
45 | consoleSpy = jest.spyOn(global.console, 'error').mockImplementation();
46 | });
47 |
48 | afterEach(() => {
49 | translator = null;
50 | jest.clearAllMocks();
51 | });
52 |
53 | it('should not do anything on node', () => {
54 | translator.translatePageTo('de');
55 |
56 | expect(consoleSpy).toHaveBeenCalledTimes(1);
57 | expect(consoleSpy).toHaveBeenCalledWith(
58 | expect.stringContaining('INVALID_ENVIRONMENT')
59 | );
60 | consoleSpy.mockClear();
61 | });
62 | });
63 |
64 | describe('fetch()', () => {
65 | let translator;
66 | let consoleSpy;
67 | const RESOURCE_FILES = {
68 | de: { title: 'Deutscher Titel', paragraph: 'Hallo Welt' },
69 | en: { title: 'English title', paragraph: 'Hello World' },
70 | };
71 |
72 | jest.mock('fs', () => ({
73 | readFileSync: jest.fn((url) => {
74 | if (url.includes('de.json')) {
75 | return JSON.stringify({
76 | title: 'Deutscher Titel',
77 | paragraph: 'Hallo Welt',
78 | });
79 | } else if (url.includes('en.json')) {
80 | return JSON.stringify({
81 | title: 'English title',
82 | paragraph: 'Hello World',
83 | });
84 | }
85 | }),
86 | }));
87 |
88 | beforeEach(() => {
89 | translator = new Translator({ debug: true });
90 | consoleSpy = jest.spyOn(global.console, 'error').mockImplementation();
91 | });
92 |
93 | it('fetches a single resource using cjs', (done) => {
94 | translator.fetch('de').then((value) => {
95 | expect(value).toMatchObject(RESOURCE_FILES['de']);
96 | expect(translator.languages.size).toBe(1);
97 | expect(translator.languages.get('de')).toMatchObject(
98 | RESOURCE_FILES['de']
99 | );
100 | done();
101 | });
102 | });
103 |
104 | it('fetches a multiple resources', (done) => {
105 | translator.fetch(['de', 'en']).then((value) => {
106 | expect(value).toMatchObject([RESOURCE_FILES['de'], RESOURCE_FILES['en']]);
107 | expect(translator.languages.size).toBe(2);
108 | done();
109 | });
110 | });
111 |
112 | it("displays an error when the resource doesn't exist", (done) => {
113 | translator.fetch('nl').then((value) => {
114 | expect(value).toBeUndefined();
115 | expect(consoleSpy).toHaveBeenCalledTimes(1);
116 | expect(consoleSpy).toHaveBeenCalledWith(
117 | expect.stringContaining('MODULE_NOT_FOUND')
118 | );
119 | done();
120 | });
121 | });
122 | });
123 |
--------------------------------------------------------------------------------
/docs/src/components/sidebar.jsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from 'react';
2 | import { Link } from 'gatsby';
3 | import * as styles from './sidebar.module.css';
4 |
5 | const TUTORIAL = 'sidebar.tutorial.open';
6 | const EXAMPLES = 'sidebar.examples.open';
7 |
8 | const Sidebar = () => {
9 | const [isTutorialOpen, setIsTutorialOpen] = useState(false);
10 | const [isExamplesOpen, setIsExamplesOpen] = useState(false);
11 |
12 | useEffect(() => {
13 | setIsTutorialOpen(Boolean(Number(localStorage.getItem(TUTORIAL))));
14 | setIsExamplesOpen(Boolean(Number(localStorage.getItem(EXAMPLES))));
15 | }, []);
16 |
17 | return (
18 |
140 | );
141 | };
142 |
143 | export default Sidebar;
144 |
--------------------------------------------------------------------------------
/docs/src/pages/tutorial/06-translating-in-javascript.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: 6. Translating in JavaScript
3 | description: Learn how to use Simple Translator in every possible use-case.
4 | slug: /tutorial/06/
5 | ---
6 |
7 | In this final section, we will have a look at how to translate content programmatically in JavaScript, without involving any HTML at all.
8 |
9 | Why is this even necessary? Not all your page content might be inside an HTML page. Think about UX flows like confirmation modals, popup notifications, or content that has been added dynamically via JavaScript. These things are usually controlled by a script and have their own text content, which is as important to translate as the rest of the page.
10 |
11 | Another example might be Node.js: _Simple Translator_ can also work with JavaScript that is not executed in the browser, but on the server-side. Say you are building a REST API or a CLI that supports translation - there's no such thing as adding `data` attributes to mark content as translateble.
12 |
13 | ## Using translateForKey()
14 |
15 | That's why _Simple Translator_ offers a method called `.translateForKey`, which allows you to translate a single key from your translations into a specific language:
16 |
17 | ```js
18 | translator.add('en', {
19 | meta: {
20 | description: 'Find the best recipes from all around the world.',
21 | },
22 | title: 'Recipes of the Day',
23 | });
24 |
25 | translator.translateForKey('meta.description', 'en');
26 | // -> "Find the best recipes from all around the world."
27 |
28 | translator.translateForKey('title', 'en');
29 | // -> "Recipes of the Day"
30 | ```
31 |
32 | You can only translate one single key at a time, but this method can be used to translate all content that you have in JavaScript. Let's look at an example on how this could be useful:
33 |
34 | ```js
35 | import Translator from '@andreasremdt/simple-translator';
36 |
37 | var translator = new Translator();
38 |
39 | translator.add('en', {
40 | confirmation: {
41 | leavePage: 'Are you sure that you want to leave this page?',
42 | },
43 | });
44 |
45 | if (
46 | window.confirm(translator.translateForKey('confirmation.leavePage', 'en'))
47 | ) {
48 | window.open('/logout');
49 | }
50 | ```
51 |
52 | ## Using the Global Helper
53 |
54 | Using a lengthy method name `.translateForKey` can be cumbersome in larger applications, that's why _Simple Translator_ offers a global shortcut. By default, you can use the global function `.__` to achieve the same result as above:
55 |
56 | ```js
57 | __('confirmation.leavePage', 'en');
58 |
59 | // or, using the default language:
60 | __('confirmation.leavePage');
61 | ```
62 |
63 | Using two underscores is a common pattern for translations, but if you'd prefer something different, you can customize the helper's name or disable it entirely:
64 |
65 | ```js
66 | import Translator from '@andreasremdt/simple-translator';
67 |
68 | var translator = new Translator({
69 | registerGlobally: 't',
70 | });
71 |
72 | t('confirmation.leavePage');
73 |
74 | // or pass `false` to disable the helper:
75 | var translator = new Translator({
76 | registerGlobally: false,
77 | });
78 | ```
79 |
80 | The global helper will work in both Node.js and browser environments. Especially when using frameworks like React.js, this might be useful, because you don't want to clutter your JSX markup with those `data-i18n` attributes nor have the translator interfere with React rendering.
81 |
82 | ```jsx
83 | function ConfirmDialog() {
84 | return (
85 |
90 | );
91 | }
92 | ```
93 |
94 | ## Conclusion
95 |
96 | Using programmatic translation might be a very useful feature for you, depending on the kind of app you are working on. In SPAs or apps built with frameworks like React.js, Vue.js, or Angular, you might want to make use of it more than `.translatePageTo`, which manipulates DOM nodes and might interfere with how these frameworks render their data.
97 |
98 | If you have a simple, server-side rendered website in plain HTML, using _Simple Translator_ to translate your entire HTML might be the better approach, though.
99 |
100 | And that's it. You made it through this tutorial and learned how to use _Simple Translator_ on your website. Go ahead and build something awesome with it, or have a look at the [examples](/examples/) for different frameworks. If you have any quesitons or issues, feel free to head over to [GitHub](https://github.com/andreasremdt/simple-translator/issues) and open a new issue.
101 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
4 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
5 |
6 | ## [2.0.4] - 2021-12-20
7 |
8 | ### Fixed
9 |
10 | - Don't depend on the `localStorage` API to exist in the environment. Ensures compatibility with Android WebView. [#154](https://github.com/andreasremdt/simple-translator/pull/154) - [@UshakovVasilii](https://github.com/UshakovVasilii)
11 |
12 | ## [2.0.3] - 2020-09-04
13 |
14 | ### Fixed
15 |
16 | - The `currentLanguage` getter now returns the correct language. If language detection is enabled, it will return the detected language by default or otherwise the default language.
17 |
18 | ## [2.0.2] - 2020-08-04
19 |
20 | ### Changed
21 |
22 | - Added compatibility for older browsers (including Safari 9) by using `Array.from` to convert a NodeList into an array.
23 |
24 | ## [2.0.1] - 2020-07-30
25 |
26 | ### Changed
27 |
28 | - Added more [CodeSandbox examples](<(https://github.com/andreasremdt/simple-translator#examples)>) to the documentation's example section.
29 |
30 | ## [2.0.0] - 2020-07-29
31 |
32 | ### Breaking changes
33 |
34 | - This release is a complete rewrite of the codebase.
35 | - The methods `load()` and `getTranslationByKey()` have been removed in favor of a new API. Use `fetch()`, `translateForKey()`, and `translatePageTo()` instead.
36 | - The config option `languages` has been removed.
37 | - For more documentation on the new API, see the [Usage section](https://github.com/andreasremdt/simple-translator#usage) or the [API Reference](https://github.com/andreasremdt/simple-translator#api-reference).
38 |
39 | ### Added
40 |
41 | - Added a config option `registerGlobally` that, if specified, registers a global helper with the same name as the given value. This allows you to translate single strings using shortcuts like `__('header.title')`.
42 | - Added a config option `persistKey` that specifies the name of the localStorage key.
43 | - Added a config option `debug` that, if set to `true`, prints useful error messages.
44 | - Added `fetch()` for easier JSON fetching.
45 | - Added `add()` to register new languages to the translator.
46 | - Added `remove()` to remove languages from the translator.
47 | - Added `translateForKey()` and `translatePageTo()` to translate single keys or the entire website.
48 | - Added `get currentLanguage` to get the currently used language.
49 | - Transpiled and minified UMD, ESM and CJS builds are available via [unpkg](https://unpkg.com/@andreasremdt/simple-translator@latest/dist/umd/translator.min.js) and [npm](https://www.npmjs.com/package/@andreasremdt/simple-translator).
50 | - Added a build system for easier packaging and testing.
51 | - Added [CONTRIBUTING.md](https://github.com/andreasremdt/simple-translator/CONTRIBUTING.md)
52 | - Added [CODE_OF_CONDUCT.md](https://github.com/andreasremdt/simple-translator/CODE_OF_CONDUCT.md)
53 |
54 | ### Changed
55 |
56 | - The [documentation](https://github.com/andreasremdt/simple-translator/#readme) has been updated and improved.
57 |
58 | ### Removed
59 |
60 | - The option `languages` has been removed.
61 | - The method `load()` has been removed.
62 | - The method `getTranslationByKey()` has been removed.
63 |
64 | ### Dependencies
65 |
66 | - Install `@babel/core@7.10.5`,
67 | - Install `@babel/plugin-proposal-optional-chaining@7.10.4`,
68 | - Install `@babel/plugin-transform-modules-commonjs@7.10.4`,
69 | - Install `@babel/preset-env@7.10.4`,
70 | - Install `@rollup/plugin-babel@5.1.0`,
71 | - Install `eslint-config-google@0.14.0`,
72 | - Install `eslint-config-prettier@6.11.0`,
73 | - Install `husky@4.2.5`,
74 | - Install `jest@26.1.0`,
75 | - Install `npm-run-all@4.1.5`,
76 | - Install `prettier@2.0.5`,
77 | - Install `rollup@2.22.2`,
78 | - Install `rollup-plugin-terser@6.1.0`
79 |
80 | ## [1.2.0] - 2020-07-21
81 |
82 | ### Added
83 |
84 | - `data-i18n-attr` can now translate multiple attributes by providing a space-separated list. Thanks [@gwprice115](https://github.com/gwprice115).
85 |
86 | ## [1.1.1] - 2020-04-05
87 |
88 | ### Changed
89 |
90 | - `getTranslationByKey` uses the fallback language when provided.
91 | - Update the documentation.
92 |
93 | ## [1.1.0] - 2020-04-04
94 |
95 | ### Added
96 |
97 | - Provide a [fallback language](https://github.com/andreasremdt/simple-translator/issues/1) using the `options.defaultLanguage` property.
98 | - Translate all [HTML attributes](https://github.com/andreasremdt/simple-translator/issues/4) like `title` or `placeholder`, not only text.
99 | - Add the method `getTranslationByKey` to translate a single key programmatically.
100 |
101 | ### Changed
102 |
103 | - Use cache to translate faster and save network data.
104 | - Print a warning message when a key was not found in the translation files. Thanks [@andi34](https://github.com/andi34).
105 |
106 | ### Dependencies
107 |
108 | - Bump eslint to 6.8.0
109 |
110 | ## [1.0.0] - 2019-10-16
111 |
112 | - Initial release
113 |
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Contributor Covenant Code of Conduct
2 |
3 | ## Our pledge
4 |
5 | We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation.
6 |
7 | We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community.
8 |
9 | ## Our standards
10 |
11 | Examples of behavior that contributes to a positive environment for our community include:
12 |
13 | - Demonstrating empathy and kindness toward other people
14 | - Being respectful of differing opinions, viewpoints, and experiences
15 | - Giving and gracefully accepting constructive feedback
16 | - Accepting responsibility and apologizing to those affected by our mistakes,
17 | and learning from the experience
18 | - Focusing on what is best not just for us as individuals, but for the
19 | overall community
20 |
21 | Examples of unacceptable behavior include:
22 |
23 | - The use of sexualized language or imagery, and sexual attention or
24 | advances of any kind
25 | - Trolling, insulting or derogatory comments, and personal or political attacks
26 | - Public or private harassment
27 | - Publishing others' private information, such as a physical or email
28 | address, without their explicit permission
29 | - Other conduct which could reasonably be considered inappropriate in a
30 | professional setting
31 |
32 | ## Enforcement responsibilities
33 |
34 | Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior. They will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful.
35 |
36 | Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that don't align with this Code of Conduct. They will communicate reasons for moderation decisions when appropriate.
37 |
38 | ## Scope
39 |
40 | This Code of Conduct applies within all community spaces and applies when an individual officially represents the community in public areas. Examples of representing our community include using an official email address, posting via an official social media account, or acting as an appointed representative at an online or offline event.
41 |
42 | ## Enforcement
43 |
44 | Instances of abusive, harassing, or otherwise, unacceptable behavior may be reported to the community leaders responsible for enforcement at [me@andreasremdt.com](mailto:me@andreasremdt.com). All complaints will be reviewed and investigated promptly and fairly.
45 |
46 | All community leaders are obligated to respect the privacy and security of the reporter of any incident.
47 |
48 | ## Enforcement guidelines
49 |
50 | Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct:
51 |
52 | ### 1. Correction
53 |
54 | **Community impact**: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community.
55 |
56 | **Consequence**: A private, written warning from community leaders provides clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested.
57 |
58 | ### 2. Warning
59 |
60 | **Community impact**: A violation through a single incident or series of actions.
61 |
62 | **Consequence**: A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited communication with those enforcing the Code of Conduct, for a specified period. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban.
63 |
64 | ### 3. Temporary ban
65 |
66 | **Community impact**: A severe violation of community standards, including sustained inappropriate behavior.
67 |
68 | **Consequence**: A temporary ban from any interaction or public communication with the community for a specified period. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban.
69 |
70 | ### 4. Permanent ban
71 |
72 | **Community impact**: Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of individuals, or aggression toward or disparagement of classes of individuals.
73 |
74 | **Consequence**: A permanent ban from any sort of public interaction within the community.
75 |
76 | ## Attribution
77 |
78 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.0, available at https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
79 |
80 | Community Impact Guidelines were inspired by [Mozilla's code of conduct enforcement ladder](https://github.com/mozilla/diversity).
81 |
82 | [homepage]: https://www.contributor-covenant.org
83 |
84 | For answers to common questions about this code of conduct, see the FAQ at https://www.contributor-covenant.org/faq. Translations are available at https://www.contributor-covenant.org/translations.
85 |
--------------------------------------------------------------------------------
/docs/src/pages/tutorial/04-preparing-translations.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: 4. Preparing the Translations
3 | description: Learn how to use Simple Translator in every possible use-case.
4 | slug: /tutorial/04/
5 | ---
6 |
7 | In the last section, we prepared the HTML by adding the `data-i18n` and `data-i18n-attr` attributes. Now it's time to look into the translations files.
8 |
9 | If you remember, we defined a key for a certain translation:
10 |
11 | ```html
12 |
Recipes of the Day
13 | ```
14 |
15 | The key (`title`) is what _Simple Translator_ will use to find a matching string, which will eventuelly replace the text content. There are two options to define translations:
16 |
17 | - A plain JavaScript object with key/value pairs
18 | - An external JSON file that you `require` or `fetch` from the server
19 |
20 | ## Option 1: JavaScript Objects
21 |
22 | The fastest way is to create a new object that contains all key/value pairs needed by the app. In our example recipe site, the object would look like that:
23 |
24 | ```js
25 | var english = {
26 | meta: {
27 | description: 'Find the best recipes from all around the world.',
28 | title: 'Delicious Recipes',
29 | },
30 | title: 'Recipes of the Day',
31 | subtitle:
32 | 'This curated list contains some fresh recipe recommendations from our chefs, ready for your kitchen.',
33 | recipes: {
34 | '1': {
35 | title: 'Rasperry Milkshake with Ginger',
36 | image: 'Image of rasperry milkshake with ginger',
37 | meta: '5 min - Easy - Shakes',
38 | },
39 | '2': {
40 | title: 'Fluffy Banana Pancakes',
41 | image: 'Image of fluffy banana pancakes',
42 | meta: '15 min - Easy - Breakfast',
43 | },
44 | },
45 | button: 'Read more',
46 | };
47 | ```
48 |
49 | You can create one object per language to store your translations. If you are using a bundler and EcmaScript imports (or Node.js), you can create separate files and import them where needed:
50 |
51 | ```js
52 | // this file is named en.js
53 | var english = { ... };
54 |
55 | export default english;
56 | ```
57 |
58 | ```js
59 | import english from './lang/en.js';
60 | import german from './lang/de.js';
61 | // add more languages if needed
62 | ```
63 |
64 | If your app is bigger, you can even split the translation files into separate, smaller ones. Using `Object.assign` or the [spread syntax](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_syntax), you can merge them when needed. Just keep in mind that keys must be unique.
65 |
66 | ```js
67 | var englishHomePage = { ... };
68 | var englishAboutPage = { ... };
69 | var english = { ...englishHomePage, ... englishAboutPage };
70 |
71 | // alternatively:
72 | var english = Object.assign({}, englishHomePage, englishAboutPage);
73 | ```
74 |
75 | ### Pros
76 |
77 | - Quick and easy way to get started, especially for smaller projects.
78 | - No fetching means no additional requests to the server and a better performance.
79 | - Translations can be manipulated dynamically with JavaScript.
80 | - Can be organized via imports.
81 |
82 | ### Cons
83 |
84 | - Translations get bundled into the final JavaScript output and might blow up the bundle size.
85 | - Depending on the folder structure, lots of translations might clutter the source code.
86 |
87 | ## Option 2: External JSON
88 |
89 | An alternative to having the translations directly as part of your JavaScript is to fetch them on demand, for example when a user switches the languages. In this case, they are stored as JSON files with the same structure as you saw above:
90 |
91 | ```json
92 | {
93 | "meta": {
94 | "description": "Find the best recipes from all around the world.",
95 | "title": "Delicious Recipes"
96 | },
97 | "title": "Recipes of the Day",
98 | "subtitle": "This curated list contains some fresh recipe recommendations from our chefs, ready for your kitchen.",
99 | "recipes": {
100 | "1": {
101 | "title": "Rasperry Milkshake with Ginger",
102 | "image": "Image of rasperry milkshake with ginger",
103 | "meta": "5 min - Easy - Shakes"
104 | },
105 | "2": {
106 | "title": "Fluffy Banana Pancakes",
107 | "image": "Image of fluffy banana pancakes",
108 | "meta": "15 min - Easy - Breakfast"
109 | }
110 | },
111 | "button": "Read more"
112 | }
113 | ```
114 |
115 | Again, if your app uses a bundler or if you are working with Node.js, you could directly import these files using EcmaScript modules or CommonJS. However, we already looked at this above.
116 |
117 | The goal here is to _fetch_ the files by either using JavaScript's Fetch API, axios, or _Simple Translator_ itself. _Simple Translator_ offers a handy method that takes care of fetching the files for you, we'll cover this soon.
118 |
119 | ### Pros
120 |
121 | - Translations are fetched on demand, lowering the initial bandwidth usage. Users only download the language(s) they need.
122 | - Translations are separated from your source code and won't get bundled, resulting in a better separation of content and code.
123 | - Can alternatively be imported via EcmaScript modules or CommonJS.
124 |
125 | ### Cons
126 |
127 | - Translations can't be manipulated dynamically via JavaScript.
128 | - Depending on the internet speed, it might take a while to fetch and update the page.
129 | - Slightly bigger overhead when getting started with _Simple Translator_.
130 |
131 | ## Conclusion
132 |
133 | With the above 2 options explained, you can go ahead and make the best decision for your project.
134 |
135 | If you are just getting started or if you need to dynamically manipulate translations and don't care about the translations ending up in your bundled output, it's recommended to use JavaScript objects directly in your source code.
136 |
137 | If you want to fetch the translations on demand, don't care about being able to manipulate your translations or don't want translations as part of your bundled output, use external JSON files and fetch them when needed.
138 |
139 | Now that we have our translations ready, let's jump to the interesting part: initializing and configuring the _Simple Translator_.
140 |
--------------------------------------------------------------------------------
/docs/src/pages/tutorial/05-translating-in-html.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: 5. Translating HTML
3 | description: Learn how to use Simple Translator in every possible use-case.
4 | slug: /tutorial/05/
5 | ---
6 |
7 | In the previous sections of this tutorial, we looked into how to prepare the HTML and translation sources, containing our text data in different languages. Now, it's time to finally use _Simple Translator_'s API to do something useful.
8 |
9 | ## Initialization
10 |
11 | In order to use the library, you have to import it first (you can skip this step if you are using the **unpkg** link):
12 |
13 | ```js
14 | import Translator from '@andreasremdt/simple-translator';
15 |
16 | // alternatively, for Node.js:
17 | var translator = require('@andreasremdt/simple-translator');
18 | ```
19 |
20 | The package `@andreasremdt/simple-translator` only contains one default export, which is the translator's class. Next, you have to create a new instance of this class:
21 |
22 | ```js
23 | var translator = new Translator();
24 | ```
25 |
26 | By default, you don't have to provide any options, but you could. Whenever you want to customize the translator's behavior, you can provide an object with some properties, like so:
27 |
28 | ```js
29 | var translator = new Translator({
30 | defaultLanguage: 'en',
31 | detectLanguage: true,
32 | selector: '[data-i18n]',
33 | debug: false,
34 | registerGlobally: '__',
35 | persist: false,
36 | persistKey: 'preferred_language',
37 | filesLocation: '/i18n',
38 | });
39 | ```
40 |
41 | You can find an overview of all available options, their default values, and what they do in the [API reference](/api/).
42 |
43 | With that out of the way, you have the `translator` instance ready to do something for you. Let's have a detailed look.
44 |
45 | ## Registering Languages
46 |
47 | Before you can translate your HTML into a certain language, you first have to register it with the translator. Otherwise, it wouldn't know where to pick the translation data from. You can register as many languages as you want.
48 |
49 | There are two ways of doing so: directly providing the JSON or fetching it from the server.
50 |
51 | ### Without Fetching
52 |
53 | If you chose to provide your translations in your JavaScript code as an object, you can use the `.add` method to register a new language:
54 |
55 | ```js
56 | translator.add('de', json);
57 | ```
58 |
59 | The first argument of `.add` is the [language code](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes) (like _en_, _de_, or _es_), the second argument is the actual JSON which contains all translations.
60 |
61 | You can also register many languages at once using method chaining:
62 |
63 | ```js
64 | translator.add('de', germanJSON).add('en', englishJSON).add('es', spanishJSON);
65 | ```
66 |
67 | You can use the translation files from the previous section and put them in here.
68 |
69 | ### With Fetching
70 |
71 | If you prefer to fetch your translations from the server using asynchronous code, _Simple Translator_ provides a handy `.fetch` method for that:
72 |
73 | ```js
74 | translator.fetch('en').then(() => {
75 | ...
76 | });
77 | ```
78 |
79 | `.fetch` returns a Promise, because under the hood, JavaScript's Fetch API is used. This means that the process of fetching your translations could take a little bit, and they won't be available immediately. If you want to use your translations afterward, you can either add a `.then` handler or use `async/await`:
80 |
81 | ```js
82 | // Using `.then`
83 | translator.fetch('en').then((englishJSON) => {
84 | console.log(englishJSON);
85 | });
86 |
87 | // Using `async/await`
88 | var englishJSON = await translator.fetch('en');
89 | ```
90 |
91 | But what happens if you want to fetch more than one language from the server? Well, `.fetch` got you covered by allowing you to pass an array of languages as the first argument:
92 |
93 | ```js
94 | translator.fetch(['en', 'de', 'es']).then((languages) => {
95 | // languages[0] -> 'en'
96 | // languages[1] -> 'de'
97 | // languages[2] -> 'es'
98 | });
99 | ```
100 |
101 | The `.fetch` method automatically registers each language after it has been loaded, using `.add` internally. You don't have to do anything else. If you want to disable this behavior and instead register the languages manually, you can provide `false` as the second argument:
102 |
103 | ```js
104 | translator.fetch('en', false).then((englishJSON) => {
105 | translator.add('en', englishJSON);
106 | });
107 | ```
108 |
109 | ## Translate The Page
110 |
111 | Now it's time to call `.translatePageTo`, which is the method that will make _Simple Translator_ translate all your HTML elements that have been marked with an `data-i18n` attribute.
112 |
113 | ```js
114 | translator.translatePageTo('de');
115 | ```
116 |
117 | If you provided a default language the option `defaultLanguage` or if you set `detectLanguage` to `true`, you can omit the argument and just call the method like so:
118 |
119 | ```js
120 | translator.translatePageTo();
121 | ```
122 |
123 | This will either choose the detected or default language.
124 |
125 | Once you call that method, you'll notice that the text on the page has changed. If the `data-i18n` attributes where set correctly and the JSON contained all keys, you should see that the elements have been translated properly. This action can be triggered after the user interacted with a button for example:
126 |
127 | ```html
128 |
129 |
130 |
131 | ```
132 |
133 | ```js
134 | for (let button of document.querySelectorAll('button')) {
135 | button.addEventListener('click', (evt) => {
136 | translator.translatePageTo(evt.target.dataset.lang);
137 | });
138 | }
139 | ```
140 |
141 | ## Conclusion
142 |
143 | Let's recap what we learned in this section. After importing the translator class, you can initialize it with some (optional) config and use `.add` to register languages synchronously or `.fetch` to register them asynchronously:
144 |
145 | **Synchronously**
146 |
147 | ```js
148 | import Translator from '@andreasremdt/simple-translator';
149 |
150 | var germanJSON = {
151 | header: {
152 | title: 'Eine Überschrift',
153 | subtitle: 'Dieser Untertitel ist nur für Demozwecke',
154 | },
155 | };
156 |
157 | var translator = new Translator();
158 |
159 | translator.add('de', germanJSON).translatePageTo('de');
160 | ```
161 |
162 | **Asynchronously**
163 |
164 | ```js
165 | import Translator from '@andreasremdt/simple-translator';
166 |
167 | // The option `filesLocation` is "/i18n" by default, but you can
168 | // override it
169 | var translator = new Translator({
170 | filesLocation: '/i18n',
171 | });
172 |
173 | // This will fetch "/i18n/de.json" and "/i18n/en.json"
174 | translator.fetch(['de', 'en']).then(() => {
175 | // You now have both languages available to you
176 | translator.translatePageTo('de');
177 | });
178 | ```
179 |
180 | In the last section of this tutorial, we'll have a look at programmatically translating strings using the `.translateForKey` method. This might come in handy when you don't have HTML to translate, but some strings inside your JavaScript code that need translation.
181 |
--------------------------------------------------------------------------------
/docs/src/pages/api.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: API Reference
3 | description: This reference provides you with an overview of all methods and configuration options.
4 | slug: /api/
5 | ---
6 |
7 | This API reference will provide you with an overview of what methods and options
8 | are available with Simple Translator. If you'd rather have a
9 | step-by-step guide on how to integrate this library into your app, [have a look here](/tutorial/).
10 |
11 | ## new Translator(Object?: options)
12 |
13 | Creates a new instance of the translator. You can define multiple instances, although this should not be a use-case. Only accepts one
14 | parameter, a JavaScript `Object`, with [some custom options](#options).
15 |
16 | ```js
17 | import Translator from '@andreasremdt/simple-translator';
18 |
19 | var translator = new Translator();
20 |
21 | // or with options:
22 | var translator = new Translator({
23 | ...
24 | });
25 | ```
26 |
27 | ### Options
28 |
29 | When initializing the `Translator` class, you can pass an object for configuration. By default, the following values apply:
30 |
31 | ```js
32 | var translator = new Translator({
33 | defaultLanguage: 'en',
34 | detectLanguage: true,
35 | selector: '[data-i18n]',
36 | debug: false,
37 | registerGlobally: '__',
38 | persist: false,
39 | persistKey: 'preferred_language',
40 | filesLocation: '/i18n',
41 | });
42 | ```
43 |
44 | | Key | Type | Default | Description |
45 | | ------------------ | --------------------- | ------------------ | ---------------------------------------------------------------------------------------------------------------------- |
46 | | `defaultLanguage` | `String` | en' | The default language, in case nothing else has been specified. |
47 | | `detectLanguage` | `Boolean` | `true` | If set to `true`, it tries to determine the user's desired language based on the browser |
48 | | `selector` | `String` | [data-i18n] | Elements that match this selector will be translated. |
49 | | `debug` | `Boolean` | `false` | When set to `true`, helpful logs will be printed to the console. Valuable for debugging and problem-solving. |
50 | | `registerGlobally` | `String` or `Boolean` | '\_\_' | When set to a `String`, it will create a global helper with the same name. When set to `false`, it won't register |
51 | | `persist` | `Boolean` | `false` | When set to `true`, the last language that was used is saved to localStorage. |
52 | | `persistKey` | `String` | preferred_language | Only valid when `persist` is set to `true`. This is the name of the key with which the last used language is stored in |
53 | | `filesLocation` | `String` | /i18n | The absolute path (from your project's root) to your localization files. |
54 |
55 | ## translateForKey(String: key, String?: language)
56 |
57 | Translates a single translation string into the desired language. If no second language parameter is provided, then:
58 |
59 | - It utilizes the last used language (accessible via the getter `currentLanguage`, but only after calling `translatePageTo()` at least once.
60 | - If no previously used language was set and the `detectLanguage` option is enabled, it uses the browser's preferred language.
61 | - If `detectLanguage` is disabled, it will fall back to the `defaultLanguage` option, which by default is `en`.
62 |
63 | ```js
64 | var translator = new Translator({
65 | defaultLanguage: 'de',
66 | });
67 |
68 | // -> translates to English (en)
69 | translator.translateForKey('header.title', 'en');
70 |
71 | // -> translates to German (de)
72 | translator.translateForKey('header.title');
73 | ```
74 |
75 | ## translatePageTo(String?: language)
76 |
77 | _Note that this method is only available in the browser and will throw an error in Node.js._
78 |
79 | Translates all DOM elements that match the selector (`'[data-i18n]'`by default) into the specified language. If no language is passed into the method, the`defaultLanguage`will be used. After this method has been called, the`Simple Translator`instance will remember the language and make it accessible via the getter`currentLanguage`.
80 |
81 | ```js
82 | var translator = new Translator({
83 | defaultLanguage: 'de',
84 | });
85 |
86 | // -> translates the page to English (en)
87 | translator.translatePageTo('en');
88 |
89 | // -> translates the page to German (de)
90 | translator.translatePageTo();
91 | ```
92 |
93 | ## add(String: language, Object: translation)
94 |
95 | Registers a new language to the translator. It must receive the language as the first and an object, containing the translation, as the second parameter. The method `add()`returns the instance of `Translator`, meaning that it can be chained.
96 |
97 | ```js
98 | translator
99 | .add('de', {...})
100 | .add('es', {...})
101 | .translatePageTo(...);
102 | ```
103 |
104 | ## remove(String: language)
105 |
106 | Removes a registered language from the translator. It accepts only the language code as a parameter. The method `remove()`returns the instance of `Translator`, meaning that it can be chained.
107 |
108 | ```js
109 | translator.remove('de');
110 | ```
111 |
112 | ## fetch(String|Array: languageFiles, Boolean?: save)
113 |
114 | Fetches either one or multiple JSON files from your project by utilizing the [Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API). By default, fetched languages are also registered to the translator instance, making them available for use. If you just want to get the JSON content, pass `false` as an optional, second parameter.
115 |
116 | You don't have to pass the entire file path or file extension (although you could). The`filesLocation` option will determine the folder. It's sufficient just to pass the language code.
117 |
118 | ```js
119 | var translator = new Translator({
120 | filesLocation: '/i18n'
121 | });
122 |
123 | // Fetches /i18n/de.json
124 | translator.fetch('de').then((json) => {
125 | console.log(json);
126 | });
127 |
128 | // Fetches "/i18n/de.json" and "/i18n/en.json"
129 | translator.fetch(['de', 'en']).then(...);
130 |
131 | // async/await
132 | // The second parameter is set to `false`, so the fetched language
133 | // will not be registered.
134 | var json = await translator.fetch('de', false);
135 | console.log(json);
136 | ```
137 |
138 | ## get currentLanguage
139 |
140 | By default, this returns the `defaultLanguage`. After calling `translatePageTo()`, this getter will return the last used language.
141 |
142 | ```js
143 | var translator = new Translator({
144 | defaultLanguage: 'de',
145 | });
146 |
147 | console.log(translator.currentLanguage);
148 | // -> "de"
149 |
150 | // Calling this methods sets the current language.
151 | translator.translatePageTo('en');
152 |
153 | console.log(translator.currentLanguage);
154 | // -> "en"
155 | ```
156 |
--------------------------------------------------------------------------------
/docs/src/pages/tutorial/02-preparing-html.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: 2. Preparing the HTML
3 | description: Learn how to prepare your HTML elements for translation by adding just two attributes.
4 | slug: /tutorial/02/
5 | prev_link: /tutorial/01/
6 | next_link: /tutorial/03/
7 | ---
8 |
9 | Before you dive into translating your website, you should look at your content and be clear about what you want to translate and what not. Not all elements might need a translation; others might need translated attributes (e.g., `title`, `placeholder`), and so on.
10 |
11 | ## Determining the Elements
12 |
13 | Let's have a look at the below HTML structure, a recipe app, as an example:
14 |
15 | ```html
16 |
17 |
18 |
19 |
20 |
21 |
25 | Delicious Recipes
26 |
27 |
28 |
Recipes of the Day
29 |
30 | This curated list contains some fresh recipe recommendations from our
31 | chefs, ready for your kitchen.
32 |
33 |
34 |
35 |
Rasperry Milkshake with Ginger
36 |
40 |
5 min - Easy - Shakes
41 |
42 |
43 |
44 |
Fluffy Banana Pancakes
45 |
49 |
15 min - Easy - Breakfast
50 |
51 |
52 |
53 |
54 | ```
55 |
56 | We need to mark the content that needs translation. It doesn't matter if your app is built with a UI framework like React or handcrafted HTML. In the end, it's the HTML elements with text content that we will hook into.
57 |
58 | Looking at the code, we can see all candidates for translation:
59 |
60 | - The `title` and `description` in the HTML's head.
61 | - The `h1` and `p`, since they put out what this page is about.
62 | - Each recipe, which is wrapped by an `article` element. A recipe has a title, some meta-information like the duration and difficulty, an image, and a button to open it.
63 |
64 | Note that there are things like the page's `author`, which we don't need nor want to translate.
65 |
66 | Coming back to the recipe (`article`) itself, you might notice that not everything that we need to translate is text content:
67 |
68 | ```html
69 |
70 | ```
71 |
72 | ```html
73 |
74 | ```
75 |
76 | Especially when considering accessibility, we also want to include attributes like `alt` or `aria-label`, because screen reader users might suffer from the lack of language consistency.
77 |
78 | ## Adding Attributes
79 |
80 | Now that we have an overview of all elements that need touching, we can go ahead and add the attributes that _Simple Translator_ will use to apply translations.
81 |
82 | For that, we have two different attributes available:
83 |
84 | - `data-i18n`: Specifies the key which is used to find a translation.
85 | - `data-i18n-attr`: Specifies which attributes should be translated instead of the text content.
86 |
87 | ### `data-i18n`
88 |
89 | Apply this attribute to all HTML elements that you want to translate, no matter if it's text content or an attribute's value:
90 |
91 | ```html
92 |
Recipes of the Day
93 |
94 | This curated list contains some fresh recipe recommendations from our chefs,
95 | ready for your kitchen.
96 |
97 | ```
98 |
99 | You can pass in any string as the value, it will be used to match the proper translation, so be mindful about it. Translations are either JSON or JavaScript objects, so they can be nested many levels deep. This means that the following syntax is also okay:
100 |
101 | ```html
102 |
Recipes of the Day
103 | ```
104 |
105 | Keep the original text hardcoded inside the HTML elements since it will be used as a fallback. Leaving it out would result in a flash of missing content when the page loads, which is considered bad practice.
106 |
107 | ### `data-i18n-attr`
108 |
109 | The above attribute is fine for translating text content, but what if we want to change the image's `alt` attribute? That's what `data-i18n-attr` is used for:
110 |
111 | ```html
112 |
118 | ```
119 |
120 | You just need to define the attribute that you want to translate. In this example, it's the image's `alt` attribute, hence we put it as the value of `data-i18n-attr`. In theory, you can provide any HTML attribute as a value, although not all of them might make sense, like `src`.
121 |
122 | The same applies for the button:
123 |
124 | ```html
125 |
132 | ```
133 |
134 | ### Translating Multiple Attributes
135 |
136 | Sometimes, you might find yourself in a situation where you need to translate more than one attribute, say for an input element:
137 |
138 | ```html
139 |
140 | ```
141 |
142 | Luckily, you can translate as many attributes as you want with `data-i18n-attr`. Just separate them with whitespaces:
143 |
144 | ```html
145 |
152 | ```
153 |
154 | Be careful to have the same amount of keys for `data-i18n` and `data-i18n-attr`. The order matters as well; make sure that you pass them in the same order as you want to translate them. Passing just one key to `data-i18n` but two attributes to `data-i18n-attr` will most likely result in unexpected behavior.
155 |
156 | ## Conclusion
157 |
158 | Let's have a look at our markup after applying the new attributes:
159 |
160 | ```html
161 |
162 |
163 |
164 |
165 |
166 |
172 | Delicious Recipes
173 |
174 |
175 |
Recipes of the Day
176 |
177 | This curated list contains some fresh recipe recommendations from our
178 | chefs, ready for your kitchen.
179 |
180 |
181 |
182 |
Rasperry Milkshake with Ginger
183 |
189 |
5 min - Easy - Shakes
190 |
193 |
194 |
195 |
Fluffy Banana Pancakes
196 |
202 |
15 min - Easy - Breakfast
203 |
206 |
207 |
208 |
209 | ```
210 |
211 | Copy this HTML into the `index.html` that you created on the previous page. We will translate it at the end of the tutorial.
212 |
213 | With that out of the way, you can head over to the next section, which will guide you through the creation of translation files in JSON.
214 |
--------------------------------------------------------------------------------
/src/translator.js:
--------------------------------------------------------------------------------
1 | import { logger } from './utils.js';
2 |
3 | /**
4 | * simple-translator
5 | * A small JavaScript library to translate webpages into different languages.
6 | * https://github.com/andreasremdt/simple-translator
7 | *
8 | * Author: Andreas Remdt (https://andreasremdt.com)
9 | * License: MIT (https://mit-license.org/)
10 | */
11 | class Translator {
12 | /**
13 | * Initialize the Translator by providing options.
14 | *
15 | * @param {Object} options
16 | */
17 | constructor(options = {}) {
18 | this.debug = logger(true);
19 |
20 | if (typeof options != 'object' || Array.isArray(options)) {
21 | this.debug('INVALID_OPTIONS', options);
22 | options = {};
23 | }
24 |
25 | this.languages = new Map();
26 | this.config = Object.assign(Translator.defaultConfig, options);
27 |
28 | const { debug, registerGlobally, detectLanguage } = this.config;
29 |
30 | this.debug = logger(debug);
31 |
32 | if (registerGlobally) {
33 | this._globalObject[registerGlobally] = this.translateForKey.bind(this);
34 | }
35 |
36 | if (detectLanguage && this._env == 'browser') {
37 | this._detectLanguage();
38 | }
39 | }
40 |
41 | /**
42 | * Return the global object, depending on the environment.
43 | * If the script is executed in a browser, return the window object,
44 | * otherwise, in Node.js, return the global object.
45 | *
46 | * @return {Object}
47 | */
48 | get _globalObject() {
49 | if (this._env == 'browser') {
50 | return window;
51 | }
52 |
53 | return global;
54 | }
55 |
56 | /**
57 | * Check and return the environment in which the script is executed.
58 | *
59 | * @return {String} The environment
60 | */
61 | get _env() {
62 | if (typeof window != 'undefined') {
63 | return 'browser';
64 | } else if (typeof module !== 'undefined' && module.exports) {
65 | return 'node';
66 | }
67 |
68 | return 'browser';
69 | }
70 |
71 | /**
72 | * Detect the users preferred language. If the language is stored in
73 | * localStorage due to a previous interaction, use it.
74 | * If no localStorage entry has been found, use the default browser language.
75 | */
76 | _detectLanguage() {
77 | const inMemory = window.localStorage
78 | ? localStorage.getItem(this.config.persistKey)
79 | : undefined;
80 |
81 | if (inMemory) {
82 | this.config.defaultLanguage = inMemory;
83 | } else {
84 | const lang = navigator.languages
85 | ? navigator.languages[0]
86 | : navigator.language;
87 |
88 | this.config.defaultLanguage = lang.substr(0, 2);
89 | }
90 | }
91 |
92 | /**
93 | * Get a translated value from a JSON by providing a key. Additionally,
94 | * the target language can be specified as the second parameter.
95 | *
96 | * @param {String} key
97 | * @param {String} toLanguage
98 | * @return {String}
99 | */
100 | _getValueFromJSON(key, toLanguage) {
101 | const json = this.languages.get(toLanguage);
102 |
103 | return key.split('.').reduce((obj, i) => (obj ? obj[i] : null), json);
104 | }
105 |
106 | /**
107 | * Replace a given DOM nodes' attribute values (by default innerHTML) with
108 | * the translated text.
109 | *
110 | * @param {HTMLElement} element
111 | * @param {String} toLanguage
112 | */
113 | _replace(element, toLanguage) {
114 | const keys = element.getAttribute('data-i18n')?.split(/\s/g);
115 | const attributes = element?.getAttribute('data-i18n-attr')?.split(/\s/g);
116 |
117 | if (attributes && keys.length != attributes.length) {
118 | this.debug('MISMATCHING_ATTRIBUTES', keys, attributes, element);
119 | }
120 |
121 | keys.forEach((key, index) => {
122 | const text = this._getValueFromJSON(key, toLanguage);
123 | const attr = attributes ? attributes[index] : 'innerHTML';
124 |
125 | if (text) {
126 | if (attr == 'innerHTML') {
127 | element[attr] = text;
128 | } else {
129 | element.setAttribute(attr, text);
130 | }
131 | } else {
132 | this.debug('TRANSLATION_NOT_FOUND', key, toLanguage);
133 | }
134 | });
135 | }
136 |
137 | /**
138 | * Translate all DOM nodes that match the given selector into the
139 | * specified target language.
140 | *
141 | * @param {String} toLanguage The target language
142 | */
143 | translatePageTo(toLanguage = this.config.defaultLanguage) {
144 | if (this._env == 'node') {
145 | this.debug('INVALID_ENVIRONMENT');
146 | return;
147 | }
148 |
149 | if (typeof toLanguage != 'string') {
150 | this.debug('INVALID_PARAM_LANGUAGE', toLanguage);
151 | return;
152 | }
153 |
154 | if (toLanguage.length == 0) {
155 | this.debug('EMPTY_PARAM_LANGUAGE');
156 | return;
157 | }
158 |
159 | if (!this.languages.has(toLanguage)) {
160 | this.debug('NO_LANGUAGE_REGISTERED', toLanguage);
161 | return;
162 | }
163 |
164 | const elements =
165 | typeof this.config.selector == 'string'
166 | ? Array.from(document.querySelectorAll(this.config.selector))
167 | : this.config.selector;
168 |
169 | if (elements.length && elements.length > 0) {
170 | elements.forEach((element) => this._replace(element, toLanguage));
171 | } else if (elements.length == undefined) {
172 | this._replace(elements, toLanguage);
173 | }
174 |
175 | this._currentLanguage = toLanguage;
176 | document.documentElement.lang = toLanguage;
177 |
178 | if (this.config.persist && window.localStorage) {
179 | localStorage.setItem(this.config.persistKey, toLanguage);
180 | }
181 | }
182 |
183 | /**
184 | * Translate a given key into the specified language if it exists
185 | * in the translation file. If not or if the language hasn't been added yet,
186 | * the return value is `null`.
187 | *
188 | * @param {String} key The key from the language file to translate
189 | * @param {String} toLanguage The target language
190 | * @return {(String|null)}
191 | */
192 | translateForKey(key, toLanguage = this.config.defaultLanguage) {
193 | if (typeof key != 'string') {
194 | this.debug('INVALID_PARAM_KEY', key);
195 | return null;
196 | }
197 |
198 | if (!this.languages.has(toLanguage)) {
199 | this.debug('NO_LANGUAGE_REGISTERED', toLanguage);
200 | return null;
201 | }
202 |
203 | const text = this._getValueFromJSON(key, toLanguage);
204 |
205 | if (!text) {
206 | this.debug('TRANSLATION_NOT_FOUND', key, toLanguage);
207 | return null;
208 | }
209 |
210 | return text;
211 | }
212 |
213 | /**
214 | * Add a translation resource to the Translator object. The language
215 | * can then be used to translate single keys or the entire page.
216 | *
217 | * @param {String} language The target language to add
218 | * @param {String} json The language resource file as JSON
219 | * @return {Object} Translator instance
220 | */
221 | add(language, json) {
222 | if (typeof language != 'string') {
223 | this.debug('INVALID_PARAM_LANGUAGE', language);
224 | return this;
225 | }
226 |
227 | if (language.length == 0) {
228 | this.debug('EMPTY_PARAM_LANGUAGE');
229 | return this;
230 | }
231 |
232 | if (Array.isArray(json) || typeof json != 'object') {
233 | this.debug('INVALID_PARAM_JSON', json);
234 | return this;
235 | }
236 |
237 | if (Object.keys(json).length == 0) {
238 | this.debug('EMPTY_PARAM_JSON');
239 | return this;
240 | }
241 |
242 | this.languages.set(language, json);
243 |
244 | return this;
245 | }
246 |
247 | /**
248 | * Remove a translation resource from the Translator object. The language
249 | * won't be available afterwards.
250 | *
251 | * @param {String} language The target language to remove
252 | * @return {Object} Translator instance
253 | */
254 | remove(language) {
255 | if (typeof language != 'string') {
256 | this.debug('INVALID_PARAM_LANGUAGE', language);
257 | return this;
258 | }
259 |
260 | if (language.length == 0) {
261 | this.debug('EMPTY_PARAM_LANGUAGE');
262 | return this;
263 | }
264 |
265 | this.languages.delete(language);
266 |
267 | return this;
268 | }
269 |
270 | /**
271 | * Fetch a translation resource from the web server. It can either fetch
272 | * a single resource or an array of resources. After all resources are fetched,
273 | * return a Promise.
274 | * If the optional, second parameter is set to true, the fetched translations
275 | * will be added to the Translator object.
276 | *
277 | * @param {String|Array} sources The files to fetch
278 | * @param {Boolean} save Save the translation to the Translator object
279 | * @return {(Promise|null)}
280 | */
281 | fetch(sources, save = true) {
282 | if (!Array.isArray(sources) && typeof sources != 'string') {
283 | this.debug('INVALID_PARAMETER_SOURCES', sources);
284 | return null;
285 | }
286 |
287 | if (!Array.isArray(sources)) {
288 | sources = [sources];
289 | }
290 |
291 | const urls = sources.map((source) => {
292 | const filename = source.replace(/\.json$/, '').replace(/^\//, '');
293 | const path = this.config.filesLocation.replace(/\/$/, '');
294 |
295 | return `${path}/${filename}.json`;
296 | });
297 |
298 | if (this._env == 'browser') {
299 | return Promise.all(urls.map((url) => fetch(url)))
300 | .then((responses) =>
301 | Promise.all(
302 | responses.map((response) => {
303 | if (response.ok) {
304 | return response.json();
305 | }
306 |
307 | this.debug('FETCH_ERROR', response);
308 | })
309 | )
310 | )
311 | .then((languageFiles) => {
312 | // If a file could not be fetched, it will be `undefined` and filtered out.
313 | languageFiles = languageFiles.filter((file) => file);
314 |
315 | if (save) {
316 | languageFiles.forEach((file, index) => {
317 | this.add(sources[index], file);
318 | });
319 | }
320 |
321 | return languageFiles.length > 1 ? languageFiles : languageFiles[0];
322 | });
323 | } else if (this._env == 'node') {
324 | return new Promise((resolve) => {
325 | const languageFiles = [];
326 |
327 | urls.forEach((url, index) => {
328 | try {
329 | const json = JSON.parse(
330 | require('fs').readFileSync(process.cwd() + url, 'utf-8')
331 | );
332 |
333 | if (save) {
334 | this.add(sources[index], json);
335 | }
336 |
337 | languageFiles.push(json);
338 | } catch (err) {
339 | this.debug('MODULE_NOT_FOUND', err.message);
340 | }
341 | });
342 |
343 | resolve(languageFiles.length > 1 ? languageFiles : languageFiles[0]);
344 | });
345 | }
346 | }
347 |
348 | /**
349 | * Sets the default language of the translator instance.
350 | *
351 | * @param {String} language
352 | * @return {void}
353 | */
354 | setDefaultLanguage(language) {
355 | if (typeof language != 'string') {
356 | this.debug('INVALID_PARAM_LANGUAGE', language);
357 | return;
358 | }
359 |
360 | if (language.length == 0) {
361 | this.debug('EMPTY_PARAM_LANGUAGE');
362 | return;
363 | }
364 |
365 | if (!this.languages.has(language)) {
366 | this.debug('NO_LANGUAGE_REGISTERED', language);
367 | return null;
368 | }
369 |
370 | this.config.defaultLanguage = language;
371 | }
372 |
373 | /**
374 | * Return the currently selected language.
375 | *
376 | * @return {String}
377 | */
378 | get currentLanguage() {
379 | return this._currentLanguage || this.config.defaultLanguage;
380 | }
381 |
382 | /**
383 | * Returns the current default language;
384 | *
385 | * @return {String}
386 | */
387 | get defaultLanguage() {
388 | return this.config.defaultLanguage;
389 | }
390 |
391 | /**
392 | * Return the default config object whose keys can be overriden
393 | * by the user's config passed to the constructor.
394 | *
395 | * @return {Object}
396 | */
397 | static get defaultConfig() {
398 | return {
399 | defaultLanguage: 'en',
400 | detectLanguage: true,
401 | selector: '[data-i18n]',
402 | debug: false,
403 | registerGlobally: '__',
404 | persist: false,
405 | persistKey: 'preferred_language',
406 | filesLocation: '/i18n',
407 | };
408 | }
409 | }
410 |
411 | export default Translator;
412 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Simple Translator
2 |
3 | > Simple, client-side translation with pure JavaScript.
4 |
5 | 
6 | 
7 | 
8 |
9 | ## Table of Contents
10 |
11 | - [The problem](#the-problem)
12 | - [The solution](#the-solution)
13 | - [Installation](#installation)
14 | - [In the browser](#in-the-browser)
15 | - [Using Node.js or bundlers](#using-nodejs-or-bundlers)
16 | - [Examples](#examples)
17 | - [Translate HTML in the browser](#translate-html-in-the-browser)
18 | - [Translate single strings](#translate-single-strings)
19 | - [Fetch JSON from the server](#fetch-json-from-the-server)
20 | - [Usage](#usage)
21 | - [Translating HTML content](#translating-html-content)
22 | - [ Translating HTML attributes](#translating-html-attributes)
23 | - [Translating programmatically](#translating-programmatically)
24 | - [Configuration](#configuration)
25 | - [API reference](#api-reference)
26 | - [new Translator(options)](#new-translatorobject-options)
27 | - _instance_
28 | - [translateForKey(key, language)](#user-content-translateforkeystring-key-string-language)
29 | - [translatePageTo(language)](#user-content-translatepagetostring-language)
30 | - [add(language, translation)](#user-content-addstring-language-object-translation)
31 | - [remove(language)](#user-content-removestring-language)
32 | - [fetch(languageFiles, save)](#user-content-fetchstringarray-languagefiles-boolean-save)
33 | - [get currentLanguage](#user-content-get-currentlanguage)
34 | - [Browser support](#browser-support)
35 | - [Issues](#issues)
36 |
37 | ## The problem
38 |
39 | You want to make your website available in multiple languages. You perhaps already looked for solutions out there and discovered various [services](https://www.i18next.com/) and [libraries](https://github.com/wikimedia/jquery.i18n), and dozens of other smaller packages that offer more or less what you are looking for.
40 |
41 | Some of them might be too grand for your purpose. You don't want to install a 100 KB dependency just for a simple translation. Or, perhaps you've found smaller libraries but are missing essential features.
42 |
43 | ## The solution
44 |
45 | `Simple Translator` is a very lightweight (~9 KB minified) solution for translating content with pure JavaScript. It works natively in the browser and Node.js.
46 |
47 | - Translate single strings
48 | - Translate entire HTML pages
49 | - Easily fetch JSON resource files (containing your translations)
50 | - Make use of global helper functions
51 | - Detect the user's preferred language automatically
52 |
53 | ## Installation
54 |
55 | ### In the browser
56 |
57 | A UMD build is available via [unpkg](https://unpkg.com). Just paste the following link into your HTML, and you're good to go:
58 |
59 | ```html
60 |
64 | ```
65 |
66 | ### Using Node.js or bundlers
67 |
68 | This package is distributed via [npm](https://npmjs.com). It's best to install it as one of your project's dependencies:
69 |
70 | ```
71 | npm i @andreasremdt/simple-translator
72 | ```
73 |
74 | Or using [yarn](https://yarnpkg.com/):
75 |
76 | ```
77 | yarn add @andreasremdt/simple-translator
78 | ```
79 |
80 | ## Examples
81 |
82 | Want to see the bigger picture? Check out the live demos at CodeSandbox and see how you can integrate the library with popular frameworks or in pure JavaScript:
83 |
84 | - [Vanilla JavaScript](https://codesandbox.io/s/simple-translator-vanilllajs-e33ye)
85 | - [React](https://codesandbox.io/s/simple-translator-react-1mtki?file=/src/Content.js:0-27)
86 | - [Vue.js](https://codesandbox.io/s/simple-translator-vuejs-iep2j?file=/src/main.js)
87 | - _Lit-Element (Web Components) currently in progress_
88 |
89 | ### Translate HTML in the browser
90 |
91 | ```html
92 |
93 |
Translate me
94 |
This subtitle is getting translated as well
95 |
96 |
97 |
98 |
102 |
118 | ```
119 |
120 | ### Translate single strings
121 |
122 | ```js
123 | // Depending on your environment, you can use CommonJS
124 | var Translator = require('@andreasremdt/simple-translator');
125 |
126 | // or EcmaScript modules
127 | import Translator from '@andreasremdt/simple-translator';
128 |
129 | // Provide your translations as JSON / JS objects
130 | var germanTranslation = {
131 | header: {
132 | title: 'Eine Überschrift',
133 | subtitle: 'Dieser Untertitel ist nur für Demozwecke',
134 | },
135 | };
136 |
137 | // You can optionally pass options
138 | var translator = new Translator();
139 |
140 | // Add the language to the translator
141 | translator.add('de', germanTranslation);
142 |
143 | // Provide single keys and the target language
144 | translator.translateForKey('header.title', 'de');
145 | translator.translateForKey('header.subtitle', 'de');
146 | ```
147 |
148 | ### Fetch JSON from the server
149 |
150 | `i18n/de.json`:
151 |
152 | ```json
153 | "header": {
154 | "title": "Eine Überschrift",
155 | "subtitle": "Dieser Untertitel ist nur für Demozwecke",
156 | }
157 | ```
158 |
159 | `i18n/en.json`:
160 |
161 | ```json
162 | "header": {
163 | "title": "Some Nice Title",
164 | "subtitle": "This Subtitle is Going to Look Good",
165 | }
166 | ```
167 |
168 | `index.js`:
169 |
170 | ```js
171 | import Translator from '@andreasremdt/simple-translator';
172 |
173 | // The option `filesLocation` is "/i18n" by default, but you can
174 | // override it
175 | var translator = new Translator({
176 | filesLocation: '/i18n',
177 | });
178 |
179 | // This will fetch "/i18n/de.json" and "/i18n/en.json"
180 | translator.fetch(['de', 'en']).then(() => {
181 | // You now have both languages available to you
182 | translator.translatePageTo('de');
183 | });
184 | ```
185 |
186 | ## Usage
187 |
188 | `Simple Translator` can be used to translate entire websites or programmatically via the API in the browser or Node.js.
189 |
190 | ### Translating HTML content
191 |
192 | > Note that this feature is only available in a browser environment and will throw an error in Node.js.
193 |
194 | In your HTML, add the `data-i18n` attribute to all DOM nodes that you want to translate. The attribute holds the key to your translation in dot syntax as if you were accessing a JavaScript object. The key resembles the structure of your translation files.
195 |
196 | ```html
197 |
198 |
Headline
199 |
200 |
201 |
Some introduction content
202 | ```
203 |
204 | Import and initialize the translator into your project's source code. The constructor accepts an optional object as [configuration](#configuration).
205 |
206 | ```js
207 | import Translator from '@andreasremdt/simple-translator';
208 |
209 | var translator = new Translator();
210 | ```
211 |
212 | Next, you need to register the translation sources. Each language has its own source and must be made available to the translator. You can either fetch them from the server or directly pass them as a JavaScript object:
213 |
214 | ```js
215 | // By using `fetch`, you load the translation sources asynchronously
216 | // from a directory in your project's folder. The resources must
217 | // be in JSON. After they are fetched, you can use the API to
218 | // translate the page.
219 | translator.fetch(['en', 'de']).then(() => {
220 | // -> Translations are ready...
221 | translator.translatePageTo('en');
222 | });
223 |
224 | // By using `add`, you pass the translation sources directly
225 | // as JavaScript objects and then use the API either through
226 | // chaining or by using the `translator` instance again.
227 | translator.add('de', jsonObject).translatePageTo('de');
228 | ```
229 |
230 | Each translation source consists of a key (the language itself, formatted in the [ISO-639-1 language code](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes)) and an object, holding key-value pairs with the translated content.
231 |
232 | Using the example above, the translation sources for each language must have the following structure:
233 |
234 | ```js
235 | {
236 | "header": {
237 | "title": "Translated title"
238 | },
239 | "intro": "The translated intro"
240 | }
241 | ```
242 |
243 | When fetching the JSON files using `fetch()`, the translator looks for a folder called `i18n` in the root of your web server. You can configure the path in the [configuration](#configuration).
244 |
245 | When a language has been registered and is ready, you can call `translatePageTo()` and provide an optional parameter for the target language, such as "en".
246 |
247 | ```js
248 | translator.translatePageTo(); // Uses the default language
249 | translator.translatePageTo('de'); // Uses German
250 | ```
251 |
252 | ### Translating HTML attributes
253 |
254 | You can translate the text content of a DOM element (it's `innerHTML`) or any other attribute, such as `title` or `placeholder`. For that, pass `data-i18n-attr` and a space-separated list of attributes to the target DOM node:
255 |
256 | ```html
257 |
263 | ```
264 |
265 | > Be careful to have the same amount of keys and attributes in `data-i18n` and `data-i18n-attr`. If you want to translate both `placeholder` and `title`, you need to pass two translation keys for it to work.
266 |
267 | By default, if `data-i18n-attr` is not defined, the innerHTML will be translated.
268 |
269 | ### Translating programmatically
270 |
271 | Instead of translating the entire page or some DOM nodes, you can translate a single, given key via `translateForKey()`. The first argument should be a key from your translation sources, such as "header.title", and the second argument should be the target language like "en" or "de". Note that the language must have been registered before calling this method.
272 |
273 | ```js
274 | translator.add('de', jsonObject);
275 |
276 | console.log(translator.translateForKey('header.title', 'de'));
277 | // -> prints the translation
278 | ```
279 |
280 | By default, `Simple Translator` registers a global helper on the `window` object to help you achieve the same without having to write the method name.
281 |
282 | ```js
283 | __.('header.title', 'de');
284 | ```
285 |
286 | > You can change the name of this helper in the [configuration](#configuration).
287 |
288 | ## Configuration
289 |
290 | When initializing the `Translator` class, you can pass an object for configuration. By default, the following values apply:
291 |
292 | ```js
293 | var translator = new Translator({
294 | defaultLanguage: 'en',
295 | detectLanguage: true,
296 | selector: '[data-i18n]',
297 | debug: false,
298 | registerGlobally: '__',
299 | persist: false,
300 | persistKey: 'preferred_language',
301 | filesLocation: '/i18n',
302 | });
303 | ```
304 |
305 | | Key | Type | Default | Description |
306 | | ---------------- | ---------------- | ---------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
307 | | defaultLanguage | `String` | `'en'` | The default language, in case nothing else has been specified. |
308 | | detectLanguage | `Boolean` | `true` | If set to `true`, it tries to determine the user's desired language based on the browser settings. |
309 | | selector | `String` | `'[data-i18n]'` | Elements that match this selector will be translated. It can be any valid [element selector](https://developer.mozilla.org/en-US/docs/Web/API/Document_object_model/Locating_DOM_elements_using_selectors). |
310 | | debug | `Boolean` | `false` | When set to `true`, helpful logs will be printed to the console. Valuable for debugging and problem-solving. |
311 | | registerGlobally | `String,Boolean` | `'__'` | When set to a `String`, it will create a global helper with the same name. When set to `false`, it won't register anything. |
312 | | persist | `Boolean` | `false` | When set to `true`, the last language that was used is saved to localStorage. |
313 | | persistKey | `String` | `'preferred_language'` | Only valid when `persist` is set to `true`. This is the name of the key with which the last used language is stored in localStorage. |
314 | | filesLocation | `String` | `'/i18n'` | The absolute path (from your project's root) to your localization files. |
315 |
316 | ## API reference
317 |
318 | ### `new Translator(Object?: options)`
319 |
320 | Creates a new instance of the translator. You can define multiple instances, although this should not be a use-case.
321 |
322 | Only accepts one parameter, a JavaScript `Object`, with a [custom config](#configuration).
323 |
324 | ```js
325 | import Translator from '@andreasremdt/simple-translator';
326 |
327 | var translator = new Translator();
328 | // or...
329 | var translator = new Translator({
330 | ...
331 | });
332 | ```
333 |
334 | ### `translateForKey(String: key, String?: language)`
335 |
336 | Translates a single translation string into the desired language. If no second language parameter is provided, then:
337 |
338 | - It utilizes the last used language (accessible via the getter `currentLanguage`, but only after calling `translatePageTo()` at least once.
339 | - If no previously used language was set and the `detectLanguage` option is enabled, it uses the browser's preferred language.
340 | - If `detectLanguage` is disabled, it will fall back to the `defaultLanguage` option, which by default is `en`.
341 |
342 | ```js
343 | var translator = new Translator({
344 | defaultLanguage: 'de',
345 | });
346 |
347 | translator.translateForKey('header.title', 'en');
348 | // -> translates to English (en)
349 | translator.translateForKey('header.title');
350 | // -> translates to German (de)
351 | ```
352 |
353 | ### `translatePageTo(String?: language)`
354 |
355 | > Note that this method is only available in the browser and will throw an error in Node.js.
356 |
357 | Translates all DOM elements that match the selector (`'[data-i18n]'` by default) into the specified language. If no language is passed into the method, the `defaultLanguage` will be used.
358 |
359 | After this method has been called, the `Simple Translator` instance will remember the language and make it accessible via the getter `currentLanguage`.
360 |
361 | ```js
362 | var translator = new Translator({
363 | defaultLanguage: 'de',
364 | });
365 |
366 | translator.translatePageTo('en');
367 | // -> translates the page to English (en)
368 | translator.translatePageTo();
369 | // -> translates the page to German (de)
370 | ```
371 |
372 | ### `add(String: language, Object: translation)`
373 |
374 | Registers a new language to the translator. It must receive the language as the first and an object, containing the translation, as the second parameter.
375 |
376 | The method `add()` returns the instance of `Simple Translator`, meaning that it can be chained.
377 |
378 | ```js
379 | translator
380 | .add('de', {...})
381 | .add('es', {...})
382 | .translatePageTo(...);
383 | ```
384 |
385 | ### `remove(String: language)`
386 |
387 | Removes a registered language from the translator. It accepts only the language code as a parameter.
388 |
389 | The method `remove()` returns the instance of `Simple Translator`, meaning that it can be chained.
390 |
391 | ```js
392 | translator.remove('de');
393 | ```
394 |
395 | ### `fetch(String|Array: languageFiles, Boolean?: save)`
396 |
397 | Fetches either one or multiple JSON files from your project by utilizing the [Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API). By default, fetched languages are also registered to the translator instance, making them available for use. If you just want to get the JSON content, pass `false` as an optional, second parameter.
398 |
399 | You don't have to pass the entire file path or file extension (although you could). The `filesLocation` option will determine folder. It's sufficient just to pass the language code.
400 |
401 | ```js
402 | var translator = new Translator({
403 | filesLocation: '/i18n'
404 | });
405 |
406 | // Fetches /i18n/de.json
407 | translator.fetch('de').then((json) => {
408 | console.log(json);
409 | });
410 |
411 | // Fetches "/i18n/de.json" and "/i18n/en.json"
412 | translator.fetch(['de', 'en']).then(...);
413 |
414 | // async/await
415 | // The second parameter is set to `false`, so the fetched language
416 | // will not be registered.
417 | await translator.fetch('de', false);
418 | ```
419 |
420 | ### `get currentLanguage`
421 |
422 | By default, this returns the `defaultLanguage`. After calling `translatePageTo()`, this getter will return the last used language.
423 |
424 | ```js
425 | var translator = new Translator({
426 | defaultLanguage: 'de',
427 | });
428 |
429 | console.log(translator.currentLanguage);
430 | // -> "de"
431 |
432 | // Calling this methods sets the current language.
433 | translator.translatePageTo('en');
434 |
435 | console.log(translator.currentLanguage);
436 | // -> "en"
437 | ```
438 |
439 | ## Browser support
440 |
441 | `Simple Translator` already comes minified and transpiled and should work in most browsers. The following browsers are tested:
442 |
443 | - Edge <= 16
444 | - Firefox <= 60
445 | - Chrome <= 61
446 | - Safari <= 10
447 | - Opera <= 48
448 |
449 | ## Issues
450 |
451 | Did you find any issues, bugs, or improvements you'd like to see implemented? Feel free to [open an issue on GitHub](https://github.com/andreasremdt/simple-translator/issues). Any feedback is appreciated.
452 |
--------------------------------------------------------------------------------
/tests/translator.browser.test.js:
--------------------------------------------------------------------------------
1 | import Translator from '../src/translator.js';
2 | import { buildHTML, removeHTML } from './test-utils.js';
3 |
4 | describe('constructor()', () => {
5 | let translator;
6 | let consoleSpy;
7 |
8 | beforeEach(() => {
9 | consoleSpy = jest.spyOn(window.console, 'error').mockImplementation();
10 | });
11 |
12 | afterEach(() => {
13 | translator = null;
14 | jest.clearAllMocks();
15 | });
16 |
17 | it('has sensible default options', () => {
18 | expect(Translator.defaultConfig).toMatchSnapshot();
19 | });
20 |
21 | it('overrides the default config with user options', () => {
22 | translator = new Translator({
23 | selector: '__',
24 | persist: true,
25 | });
26 |
27 | expect(translator.config).toMatchObject({
28 | selector: '__',
29 | persist: true,
30 | detectLanguage: true,
31 | filesLocation: '/i18n',
32 | });
33 | });
34 |
35 | it('creates a global helper', () => {
36 | translator = new Translator();
37 |
38 | expect(window.__).toBeDefined();
39 |
40 | translator = new Translator({ registerGlobally: 't' });
41 |
42 | expect(window.t).toBeDefined();
43 |
44 | delete window.__;
45 | delete window.t;
46 | });
47 |
48 | it('should not create a global helper when turned off', () => {
49 | translator = new Translator({ registerGlobally: false });
50 |
51 | expect(window.__).toBeUndefined();
52 | });
53 |
54 | it('detects the correct language automatically', () => {
55 | const languageGetter = jest.spyOn(window.navigator, 'languages', 'get');
56 |
57 | expect(new Translator().config.defaultLanguage).toBe('en');
58 |
59 | languageGetter.mockReturnValue(['de-DE', 'de']);
60 |
61 | expect(new Translator().config.defaultLanguage).toBe('de');
62 |
63 | languageGetter.mockReset();
64 | });
65 |
66 | it('should not detect the default language when turned off', () => {
67 | translator = new Translator({ detectLanguage: false });
68 |
69 | expect(translator.config.defaultLanguage).toBe('en');
70 | });
71 |
72 | it('validates the user options', () => {
73 | translator = new Translator([]);
74 | translator = new Translator(false);
75 | translator = new Translator('test');
76 |
77 | expect(translator.config).toMatchObject(Translator.defaultConfig);
78 | expect(consoleSpy).toHaveBeenCalledTimes(3);
79 | expect(consoleSpy).toHaveBeenCalledWith(
80 | expect.stringContaining('INVALID_OPTIONS')
81 | );
82 | });
83 |
84 | it('reads the last used language from localStorage', () => {
85 | localStorage.setItem('preferred_language', 'de');
86 |
87 | translator = new Translator();
88 |
89 | expect(translator.config.defaultLanguage).toBe('de');
90 |
91 | localStorage.removeItem('preferred_language');
92 | localStorage.setItem('custom_language', 'nl');
93 |
94 | translator = new Translator({
95 | persistKey: 'custom_language',
96 | });
97 |
98 | expect(translator.config.defaultLanguage).toBe('nl');
99 |
100 | localStorage.removeItem('custom_language');
101 | });
102 |
103 | it('should not print console errors when debugging is turned off', () => {
104 | translator = new Translator();
105 |
106 | translator.add({});
107 |
108 | expect(consoleSpy).toHaveBeenCalledTimes(0);
109 | });
110 | });
111 |
112 | describe('add()', () => {
113 | let translator;
114 | let consoleSpy;
115 |
116 | beforeEach(() => {
117 | translator = new Translator({ debug: true });
118 | consoleSpy = jest.spyOn(window.console, 'error').mockImplementation();
119 | });
120 |
121 | afterEach(() => {
122 | translator = null;
123 | jest.clearAllMocks();
124 | });
125 |
126 | it('adds a single language', () => {
127 | expect(translator.languages.size).toBe(0);
128 |
129 | translator.add('de', { title: 'German title' });
130 |
131 | expect(translator.languages.size).toBe(1);
132 | expect(translator.languages.get('de')).toMatchObject({
133 | title: 'German title',
134 | });
135 | });
136 |
137 | it('adds multiple languages using chaining', () => {
138 | expect(translator.languages.size).toBe(0);
139 |
140 | translator
141 | .add('de', { title: 'German title' })
142 | .add('en', { title: 'English title' });
143 |
144 | expect(translator.languages.size).toBe(2);
145 | expect(translator.languages.get('de')).toMatchObject({
146 | title: 'German title',
147 | });
148 | });
149 |
150 | it('requires a valid language key', () => {
151 | translator.add();
152 |
153 | expect(consoleSpy).toHaveBeenCalledTimes(1);
154 | expect(consoleSpy).toHaveBeenCalledWith(
155 | expect.stringContaining('INVALID_PARAM_LANGUAGE')
156 | );
157 | consoleSpy.mockClear();
158 |
159 | translator.add(true).add({}).add([]).add(1);
160 |
161 | expect(consoleSpy).toHaveBeenCalledTimes(4);
162 | expect(consoleSpy).toHaveBeenCalledWith(
163 | expect.stringContaining('INVALID_PARAM_LANGUAGE')
164 | );
165 | consoleSpy.mockClear();
166 |
167 | translator.add('');
168 |
169 | expect(consoleSpy).toHaveBeenCalledTimes(1);
170 | expect(consoleSpy).toHaveBeenCalledWith(
171 | expect.stringContaining('EMPTY_PARAM_LANGUAGE')
172 | );
173 | expect(translator.languages.size).toBe(0);
174 | });
175 |
176 | it('requires a valid json translation', () => {
177 | translator
178 | .add('de')
179 | .add('de', true)
180 | .add('de', 1)
181 | .add('de', 'text')
182 | .add('de', []);
183 |
184 | expect(consoleSpy).toHaveBeenCalledTimes(5);
185 | expect(consoleSpy).toHaveBeenCalledWith(
186 | expect.stringContaining('INVALID_PARAM_JSON')
187 | );
188 | consoleSpy.mockClear();
189 |
190 | translator.add('de', {});
191 |
192 | expect(consoleSpy).toHaveBeenCalledTimes(1);
193 | expect(consoleSpy).toHaveBeenCalledWith(
194 | expect.stringContaining('EMPTY_PARAM_JSON')
195 | );
196 | expect(translator.languages.size).toBe(0);
197 | });
198 | });
199 |
200 | describe('remove()', () => {
201 | let translator;
202 | let consoleSpy;
203 |
204 | beforeEach(() => {
205 | translator = new Translator({ debug: true });
206 | translator
207 | .add('de', { title: 'German title' })
208 | .add('en', { title: 'English title' });
209 | consoleSpy = jest.spyOn(window.console, 'error').mockImplementation();
210 | });
211 |
212 | afterEach(() => {
213 | translator = null;
214 | jest.clearAllMocks();
215 | });
216 |
217 | it('removes an existing language', () => {
218 | translator.remove('de');
219 |
220 | expect(translator.languages.size).toBe(1);
221 | expect(translator.languages.get('de')).toBeUndefined();
222 | expect(translator.languages.get('en')).toBeDefined();
223 | });
224 |
225 | it('removes multiple existing languages', () => {
226 | translator.remove('de').remove('en');
227 |
228 | expect(translator.languages.size).toBe(0);
229 | });
230 |
231 | it("doesn't remove anything when the given language doesn't exist", () => {
232 | translator.remove('nl');
233 |
234 | expect(translator.languages.size).toBe(2);
235 | expect(translator.languages.get('de')).toBeDefined();
236 | expect(translator.languages.get('en')).toBeDefined();
237 | });
238 |
239 | it('requires a valid language key', () => {
240 | translator.remove(true).remove({}).remove([]).remove(1);
241 |
242 | expect(consoleSpy).toHaveBeenCalledTimes(4);
243 | expect(consoleSpy).toHaveBeenCalledWith(
244 | expect.stringContaining('INVALID_PARAM_LANGUAGE')
245 | );
246 | consoleSpy.mockClear();
247 |
248 | translator.remove('');
249 |
250 | expect(consoleSpy).toHaveBeenCalledTimes(1);
251 | expect(consoleSpy).toHaveBeenCalledWith(
252 | expect.stringContaining('EMPTY_PARAM_LANGUAGE')
253 | );
254 | expect(translator.languages.size).toBe(2);
255 | });
256 | });
257 |
258 | describe('translateForKey()', () => {
259 | let translator;
260 | let consoleSpy;
261 |
262 | beforeEach(() => {
263 | translator = new Translator({ debug: true });
264 | translator
265 | .add('de', { title: 'German title' })
266 | .add('en', { title: 'English title' });
267 | consoleSpy = jest.spyOn(window.console, 'error').mockImplementation();
268 | });
269 |
270 | afterEach(() => {
271 | translator = null;
272 | jest.clearAllMocks();
273 | });
274 |
275 | it('returns a string with the translated text', () => {
276 | const text = translator.translateForKey('title', 'de');
277 |
278 | expect(text).toBe('German title');
279 | });
280 |
281 | it('uses the default language (en) if no second parameter is provided', () => {
282 | const text = translator.translateForKey('title');
283 |
284 | expect(text).toBe('English title');
285 | });
286 |
287 | it('works with the global helper', () => {
288 | const text = window.__('title', 'de');
289 |
290 | expect(text).toBe('German title');
291 | });
292 |
293 | it('displays an error when no translation has been found', () => {
294 | const text = translator.translateForKey('not.existing');
295 |
296 | expect(text).toBeNull();
297 | expect(consoleSpy).toHaveBeenCalledTimes(1);
298 | expect(consoleSpy).toHaveBeenCalledWith(
299 | expect.stringContaining('TRANSLATION_NOT_FOUND')
300 | );
301 | });
302 |
303 | it('requires a valid language key', () => {
304 | const texts = [
305 | translator.translateForKey({}),
306 | translator.translateForKey(false),
307 | translator.translateForKey(1),
308 | translator.translateForKey(''),
309 | ];
310 |
311 | expect(texts).toMatchObject([null, null, null, null]);
312 | expect(consoleSpy).toHaveBeenCalledTimes(4);
313 | expect(consoleSpy).toHaveBeenCalledWith(
314 | expect.stringContaining('INVALID_PARAM_KEY')
315 | );
316 | });
317 |
318 | it("displays an error when the target language doesn't exist", () => {
319 | const text = translator.translateForKey('title', 'nl');
320 |
321 | expect(text).toBeNull();
322 | expect(consoleSpy).toHaveBeenCalledTimes(1);
323 | expect(consoleSpy).toHaveBeenCalledWith(
324 | expect.stringContaining('NO_LANGUAGE_REGISTERED')
325 | );
326 | });
327 | });
328 |
329 | describe('translatePageTo()', () => {
330 | let translator;
331 | let consoleSpy;
332 |
333 | beforeEach(() => {
334 | translator = new Translator({ debug: true });
335 | translator
336 | .add('de', { title: 'Deutscher Titel', paragraph: 'Hallo Welt' })
337 | .add('en', { title: 'English title', paragraph: 'Hello World' });
338 | consoleSpy = jest.spyOn(window.console, 'error').mockImplementation();
339 | });
340 |
341 | afterEach(() => {
342 | translator = null;
343 | jest.clearAllMocks();
344 | });
345 |
346 | it("translates an element's innerHTML to the given language", () => {
347 | // Tests the following:
348 | //
349 | //
350 | //
351 | const { h1, p } = buildHTML({
352 | title: { keys: 'title' },
353 | paragraph: { keys: 'paragraph' },
354 | });
355 |
356 | expect(h1.textContent).toBe('Default heading text');
357 | expect(p.textContent).toBe('Default content text');
358 |
359 | translator.translatePageTo('de');
360 |
361 | expect(h1.textContent).toBe('Deutscher Titel');
362 | expect(p.textContent).toBe('Hallo Welt');
363 |
364 | translator.translatePageTo('en');
365 |
366 | expect(h1.textContent).toBe('English title');
367 | expect(p.textContent).toBe('Hello World');
368 |
369 | removeHTML(h1, p);
370 | });
371 |
372 | it("translates an element's innerHTML to the default language", () => {
373 | // Tests the following:
374 | //
375 | //
376 | //
377 | const { h1, p } = buildHTML({
378 | title: {
379 | keys: 'title',
380 | },
381 | paragraph: {
382 | keys: 'paragraph',
383 | },
384 | });
385 |
386 | translator.translatePageTo();
387 |
388 | expect(h1.textContent).toBe('English title');
389 | expect(p.textContent).toBe('Hello World');
390 |
391 | removeHTML(h1, p);
392 | });
393 |
394 | it('persists the last used language in localStorage', () => {
395 | // Tests the following:
396 | //
397 | //
398 | const { h1, p } = buildHTML({
399 | title: {
400 | keys: 'title',
401 | },
402 | paragraph: {
403 | keys: 'paragraph',
404 | },
405 | });
406 |
407 | translator.config.persist = true;
408 | translator.translatePageTo('de');
409 |
410 | expect(localStorage.getItem('preferred_language')).toBe('de');
411 |
412 | localStorage.removeItem('preferred_language');
413 | translator.config.persistKey = 'custom_language';
414 | translator.translatePageTo('de');
415 |
416 | expect(localStorage.getItem('preferred_language')).toBeNull();
417 | expect(localStorage.getItem('custom_language')).toBe('de');
418 |
419 | localStorage.removeItem('custom_language');
420 |
421 | removeHTML(h1, p);
422 | });
423 |
424 | it('uses a custom selector when provided', () => {
425 | // Tests the following:
426 | //
427 | //
428 | //
429 | const { h1, p } = buildHTML({
430 | title: {
431 | keys: 'title',
432 | },
433 | paragraph: {
434 | keys: 'paragraph',
435 | },
436 | });
437 |
438 | translator.config.selector = document.querySelectorAll('h1');
439 | translator.translatePageTo('de');
440 |
441 | expect(h1.textContent).toBe('Deutscher Titel');
442 | expect(p.textContent).toBe('Default content text');
443 |
444 | translator.config.selector = document.querySelector('p');
445 | translator.translatePageTo('de');
446 |
447 | expect(p.textContent).toBe('Hallo Welt');
448 |
449 | removeHTML(h1, p);
450 | });
451 |
452 | it("doesn't do anything when no elements match the selector", () => {
453 | // Tests the following:
454 | //
455 | //
456 | //
457 | const { h1, p } = buildHTML({
458 | title: {
459 | keys: 'title',
460 | },
461 | paragraph: {
462 | keys: 'paragraph',
463 | },
464 | });
465 |
466 | translator.config.selector = document.querySelectorAll('span');
467 | translator.translatePageTo('de');
468 |
469 | expect(h1.textContent).toBe('Default heading text');
470 | expect(p.textContent).toBe('Default content text');
471 |
472 | removeHTML(h1, p);
473 | });
474 |
475 | it("translates an element's custom attribute", () => {
476 | // Tests the following:
477 | //
478 | //
479 | //
480 | const { h1, p } = buildHTML({
481 | title: {
482 | attrs: 'title',
483 | keys: 'title',
484 | },
485 | paragraph: {
486 | attrs: 'data-msg',
487 | keys: 'paragraph',
488 | },
489 | });
490 |
491 | translator.translatePageTo('de');
492 |
493 | expect(h1.getAttribute('title')).toBe('Deutscher Titel');
494 | expect(p.getAttribute('data-msg')).toBe('Hallo Welt');
495 |
496 | removeHTML(h1, p);
497 | });
498 |
499 | it('translates multiple custom attributes', () => {
500 | // Tests the following:
501 | //
502 | //
503 | const { h1, p } = buildHTML({
504 | title: {
505 | attrs: 'title data-msg',
506 | keys: 'title paragraph',
507 | },
508 | paragraph: {
509 | keys: 'paragraph',
510 | },
511 | });
512 |
513 | translator.translatePageTo('de');
514 |
515 | expect(h1.getAttribute('title')).toBe('Deutscher Titel');
516 | expect(h1.getAttribute('data-msg')).toBe('Hallo Welt');
517 |
518 | removeHTML(h1, p);
519 | });
520 |
521 | it('requires the same amount of keys and attributes to translate', () => {
522 | // Tests the following:
523 | //
524 | //
525 | const { h1, p } = buildHTML({
526 | title: {
527 | attrs: 'title data-msg',
528 | keys: 'title',
529 | },
530 | paragraph: {
531 | keys: 'paragraph',
532 | },
533 | });
534 |
535 | translator.translatePageTo('de');
536 |
537 | expect(consoleSpy).toHaveBeenCalledTimes(1);
538 | expect(consoleSpy).toHaveBeenCalledWith(
539 | expect.stringContaining('MISMATCHING_ATTRIBUTES')
540 | );
541 | consoleSpy.mockClear();
542 |
543 | expect(h1.getAttribute('title')).toBe('Deutscher Titel');
544 | expect(h1.getAttribute('data-msg')).toBeNull();
545 |
546 | removeHTML(h1, p);
547 | });
548 |
549 | it('displays an error when no translation has been found', () => {
550 | // Tests the following:
551 | //
552 | //
553 | //
554 | const { h1, p } = buildHTML({
555 | title: {
556 | keys: 'title',
557 | },
558 | paragraph: {
559 | keys: 'not.existing',
560 | },
561 | });
562 |
563 | translator.translatePageTo('de');
564 |
565 | expect(consoleSpy).toHaveBeenCalledTimes(1);
566 | expect(consoleSpy).toHaveBeenCalledWith(
567 | expect.stringContaining('TRANSLATION_NOT_FOUND')
568 | );
569 | consoleSpy.mockClear();
570 |
571 | expect(h1.textContent).toBe('Deutscher Titel');
572 | expect(p.textContent).toBe('Default content text');
573 |
574 | removeHTML(h1, p);
575 | });
576 |
577 | it('requires a valid language key', () => {
578 | translator.translatePageTo(false);
579 | translator.translatePageTo({});
580 | translator.translatePageTo([]);
581 |
582 | expect(consoleSpy).toHaveBeenCalledTimes(3);
583 | expect(consoleSpy).toHaveBeenCalledWith(
584 | expect.stringContaining('INVALID_PARAM_LANGUAGE')
585 | );
586 | consoleSpy.mockClear();
587 |
588 | translator.translatePageTo('');
589 |
590 | expect(consoleSpy).toHaveBeenCalledTimes(1);
591 | expect(consoleSpy).toHaveBeenCalledWith(
592 | expect.stringContaining('EMPTY_PARAM_LANGUAGE')
593 | );
594 | consoleSpy.mockClear();
595 |
596 | translator.translatePageTo('nl');
597 |
598 | expect(consoleSpy).toHaveBeenCalledTimes(1);
599 | expect(consoleSpy).toHaveBeenCalledWith(
600 | expect.stringContaining('NO_LANGUAGE_REGISTERED')
601 | );
602 | });
603 |
604 | it('changes the `lang` attribute', () => {
605 | translator.translatePageTo('de');
606 |
607 | expect(document.documentElement.lang).toBe('de');
608 |
609 | translator.translatePageTo();
610 |
611 | expect(document.documentElement.lang).toBe('en');
612 | });
613 | });
614 |
615 | describe('fetch()', () => {
616 | let translator;
617 | let consoleSpy;
618 | const RESOURCE_FILES = {
619 | de: { title: 'Deutscher Titel', paragraph: 'Hallo Welt' },
620 | en: { title: 'English title', paragraph: 'Hello World' },
621 | };
622 |
623 | beforeEach(() => {
624 | translator = new Translator({ debug: true });
625 | consoleSpy = jest.spyOn(window.console, 'error').mockImplementation();
626 |
627 | global.fetch = jest.fn().mockImplementation((url) => {
628 | url = url.replace(/\/i18n\//, '').replace(/\.json/, '');
629 |
630 | return Promise.resolve({
631 | ok: url == 'nl' ? false : true,
632 | json: () => Promise.resolve(RESOURCE_FILES[url]),
633 | });
634 | });
635 | });
636 |
637 | afterEach(() => {
638 | translator = null;
639 | jest.clearAllMocks();
640 | delete global.fetch;
641 | });
642 |
643 | it('fetches a single resource', (done) => {
644 | translator.fetch('de').then((value) => {
645 | expect(value).toMatchObject(RESOURCE_FILES['de']);
646 | expect(translator.languages.size).toBe(1);
647 | expect(translator.languages.get('de')).toMatchObject(
648 | RESOURCE_FILES['de']
649 | );
650 | done();
651 | });
652 | });
653 |
654 | it('fetches a multiple resources', (done) => {
655 | translator.fetch(['de', 'en']).then((value) => {
656 | expect(value).toMatchObject([RESOURCE_FILES['de'], RESOURCE_FILES['en']]);
657 | expect(translator.languages.size).toBe(2);
658 | done();
659 | });
660 | });
661 |
662 | it("displays an error when the resource doesn't exist", (done) => {
663 | translator.fetch('nl').then((value) => {
664 | expect(value).toBeUndefined();
665 | expect(consoleSpy).toHaveBeenCalledTimes(1);
666 | expect(consoleSpy).toHaveBeenCalledWith(
667 | expect.stringContaining('FETCH_ERROR')
668 | );
669 | done();
670 | });
671 | });
672 |
673 | it('fetches available resources and displays an error for non-existing resources', (done) => {
674 | translator.fetch(['de', 'nl']).then((value) => {
675 | expect(value).toMatchObject(RESOURCE_FILES['de']);
676 | expect(translator.languages.size).toBe(1);
677 | expect(consoleSpy).toHaveBeenCalledTimes(1);
678 | expect(consoleSpy).toHaveBeenCalledWith(
679 | expect.stringContaining('FETCH_ERROR')
680 | );
681 | done();
682 | });
683 | });
684 |
685 | it("only fetches and doesn't save the resources", (done) => {
686 | translator.fetch('de', false).then((value) => {
687 | expect(value).toMatchObject(RESOURCE_FILES['de']);
688 | expect(translator.languages.size).toBe(0);
689 | done();
690 | });
691 | });
692 |
693 | it('accepts sources with and without file extension', (done) => {
694 | translator.fetch(['/de.json', 'en']).then((value) => {
695 | expect(value.length).toBe(2);
696 | done();
697 | });
698 | });
699 |
700 | it('requires a valid sources parameter', () => {
701 | translator.fetch(true);
702 | translator.fetch({});
703 | translator.fetch(1);
704 | translator.fetch();
705 |
706 | expect(consoleSpy).toHaveBeenCalledTimes(4);
707 | expect(consoleSpy).toHaveBeenCalledWith(
708 | expect.stringContaining('INVALID_PARAMETER_SOURCES')
709 | );
710 | });
711 | });
712 |
713 | describe('setDefaultLanguage()', () => {
714 | let translator;
715 | let consoleSpy;
716 |
717 | beforeEach(() => {
718 | translator = new Translator({ debug: true });
719 | translator
720 | .add('de', { title: 'Deutscher Titel', paragraph: 'Hallo Welt' })
721 | .add('en', { title: 'English title', paragraph: 'Hello World' });
722 | consoleSpy = jest.spyOn(window.console, 'error').mockImplementation();
723 | });
724 |
725 | afterEach(() => {
726 | translator = null;
727 | jest.clearAllMocks();
728 | });
729 |
730 | it('sets a new default language', () => {
731 | translator.setDefaultLanguage('de');
732 |
733 | expect(translator.translateForKey('title')).toBe('Deutscher Titel');
734 | expect(translator.config.defaultLanguage).toBe('de');
735 |
736 | translator.setDefaultLanguage('en');
737 |
738 | expect(translator.translateForKey('title')).toBe('English title');
739 | expect(translator.config.defaultLanguage).toBe('en');
740 | });
741 |
742 | it("displays an error when the given language isn't registered", () => {
743 | translator.setDefaultLanguage('es');
744 |
745 | expect(consoleSpy).toHaveBeenCalledTimes(1);
746 | expect(consoleSpy).toHaveBeenCalledWith(
747 | expect.stringContaining('NO_LANGUAGE_REGISTERED')
748 | );
749 | });
750 |
751 | it('displays an error when the given language is invalid', () => {
752 | translator.setDefaultLanguage();
753 | translator.setDefaultLanguage('');
754 | translator.setDefaultLanguage(false);
755 | translator.setDefaultLanguage({});
756 | translator.setDefaultLanguage([]);
757 |
758 | expect(consoleSpy).toHaveBeenCalledTimes(5);
759 | expect(consoleSpy).toHaveBeenCalledWith(
760 | expect.stringContaining('INVALID_PARAM_LANGUAGE')
761 | );
762 | expect(consoleSpy).toHaveBeenCalledWith(
763 | expect.stringContaining('EMPTY_PARAM_LANGUAGE')
764 | );
765 | });
766 | });
767 |
768 | describe('get currentLanguage()', () => {
769 | let languageGetter;
770 |
771 | beforeEach(() => {
772 | languageGetter = jest.spyOn(window.navigator, 'languages', 'get');
773 | languageGetter.mockReturnValue(['de-DE', 'de']);
774 | });
775 |
776 | afterEach(() => {
777 | jest.clearAllMocks();
778 | });
779 |
780 | it('returns the correct language code with auto-detection', () => {
781 | const translator = new Translator();
782 | translator
783 | .add('de', { title: 'Deutscher Titel', paragraph: 'Hallo Welt' })
784 | .add('en', { title: 'English title', paragraph: 'Hello World' });
785 |
786 | expect(translator.currentLanguage).toBe('de');
787 | translator.translatePageTo('en');
788 | expect(translator.currentLanguage).toBe('en');
789 | });
790 |
791 | it('returns the correct language code without auto-detection', () => {
792 | const translator = new Translator({
793 | detectLanguage: false,
794 | defaultLanguage: 'de',
795 | });
796 | translator
797 | .add('de', { title: 'Deutscher Titel', paragraph: 'Hallo Welt' })
798 | .add('en', { title: 'English title', paragraph: 'Hello World' });
799 |
800 | expect(translator.currentLanguage).toBe('de');
801 | translator.translatePageTo('en');
802 | expect(translator.currentLanguage).toBe('en');
803 | });
804 | });
805 |
--------------------------------------------------------------------------------