From 9b32bd06007b0ad31bd3143651abb567aeec2c4a Mon Sep 17 00:00:00 2001 From: Maksym Stoyanov Date: Fri, 16 Aug 2024 17:38:22 +0200 Subject: [PATCH] git init --- LICENSE.md | 21 ++ README.md | 9 + SECURITY.md | 9 + docs/bg/CHANGELOG.md | 24 ++ docs/bg/CONTRIBUTING.md | 10 + docs/bg/LICENSE.md | 30 ++ docs/bg/README.md | 108 +++++++ docs/bg/SECURITY.md | 10 + docs/de/CHANGELOG.md | 24 ++ docs/de/CONTRIBUTING.md | 10 + docs/de/LICENSE.md | 30 ++ docs/de/README.md | 108 +++++++ docs/de/SECURITY.md | 10 + docs/en/CHANGELOG.md | 24 ++ docs/en/CONTRIBUTING.md | 10 + docs/en/LICENSE.md | 30 ++ docs/en/README.md | 108 +++++++ docs/en/SECURITY.md | 10 + docs/ru/CHANGELOG.md | 24 ++ docs/ru/CONTRIBUTING.md | 10 + docs/ru/LICENSE.md | 30 ++ docs/ru/README.md | 108 +++++++ docs/ru/SECURITY.md | 10 + docs/uk/CHANGELOG.md | 24 ++ docs/uk/CONTRIBUTING.md | 10 + docs/uk/LICENSE.md | 30 ++ docs/uk/README.md | 108 +++++++ docs/uk/SECURITY.md | 10 + src/appsscript.json | 4 + src/i18n.js | 686 ++++++++++++++++++++++++++++++++++++++++ src/tests.js | 0 31 files changed, 1639 insertions(+) create mode 100644 LICENSE.md create mode 100644 README.md create mode 100644 SECURITY.md create mode 100644 docs/bg/CHANGELOG.md create mode 100644 docs/bg/CONTRIBUTING.md create mode 100644 docs/bg/LICENSE.md create mode 100644 docs/bg/README.md create mode 100644 docs/bg/SECURITY.md create mode 100644 docs/de/CHANGELOG.md create mode 100644 docs/de/CONTRIBUTING.md create mode 100644 docs/de/LICENSE.md create mode 100644 docs/de/README.md create mode 100644 docs/de/SECURITY.md create mode 100644 docs/en/CHANGELOG.md create mode 100644 docs/en/CONTRIBUTING.md create mode 100644 docs/en/LICENSE.md create mode 100644 docs/en/README.md create mode 100644 docs/en/SECURITY.md create mode 100644 docs/ru/CHANGELOG.md create mode 100644 docs/ru/CONTRIBUTING.md create mode 100644 docs/ru/LICENSE.md create mode 100644 docs/ru/README.md create mode 100644 docs/ru/SECURITY.md create mode 100644 docs/uk/CHANGELOG.md create mode 100644 docs/uk/CONTRIBUTING.md create mode 100644 docs/uk/LICENSE.md create mode 100644 docs/uk/README.md create mode 100644 docs/uk/SECURITY.md create mode 100644 src/appsscript.json create mode 100644 src/i18n.js create mode 100644 src/tests.js diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..21d1b5c --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,21 @@ +# MIT License + +Copyright (c) 2023 Maksym Stoianov + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..efaefcb --- /dev/null +++ b/README.md @@ -0,0 +1,9 @@ +

Readme

+ +
+ Български + Deutsch + English + Русский + Українська +
\ No newline at end of file diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..c2b5a7b --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,9 @@ +

Security policy

+ +
+ Български + Deutsch + English + Русский + Українська +
\ No newline at end of file diff --git a/docs/bg/CHANGELOG.md b/docs/bg/CHANGELOG.md new file mode 100644 index 0000000..96b007e --- /dev/null +++ b/docs/bg/CHANGELOG.md @@ -0,0 +1,24 @@ +
+ Български + Deutsch + English + Русский + Українська +
+ + +# Changelog + + +## [1.1.2] - 2024-08-16 + +### Подобрения +* Подобрена документация на JSDoc за по-голяма яснота и детайлност. + +### Корекции +* Решени са различни грешки и проблеми. + + +## [1.0.0] - 2023-10-29 + +* Първоначална версия. \ No newline at end of file diff --git a/docs/bg/CONTRIBUTING.md b/docs/bg/CONTRIBUTING.md new file mode 100644 index 0000000..b6f9a98 --- /dev/null +++ b/docs/bg/CONTRIBUTING.md @@ -0,0 +1,10 @@ +
+ Български + Deutsch + English + Русский + Українська +
+ + +# Как да станете участник? diff --git a/docs/bg/LICENSE.md b/docs/bg/LICENSE.md new file mode 100644 index 0000000..91b95cb --- /dev/null +++ b/docs/bg/LICENSE.md @@ -0,0 +1,30 @@ +
+ Български + Deutsch + English + Русский + Українська +
+ + +# MIT License + +Copyright (c) 2023 Maksym Stoianov + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/docs/bg/README.md b/docs/bg/README.md new file mode 100644 index 0000000..932ef3c --- /dev/null +++ b/docs/bg/README.md @@ -0,0 +1,108 @@ +
+ Български + Deutsch + English + Русский + Українська +
+ + +# I18nService + +
+ Release + License + clasp +
+ +**I18nService** е обект, който реализира работа с интернационализация. + + +## Инсталация + +1. Отворете своя проект в [Google Apps Script Dashboard](https://script.google.com/). +2. Копирайте съдържанието на файла [i18n.js](../../src/i18n.js) и го поставете в нов файл във вашия проект в Google Apps Script. + + +## Употреба + +### Пример 1 + +```javascript +const data = { + "bg": { + title: "Тестово приложение" + }, + "de": { + title: "Testanwendung" + }, + "en": { + title: "Example Application" + }, + "ru": { + title: "Тестовое приложение" + }, + "uk": { + title: "Тестовий застосунок" + } +}; + +const i18n = I18nService + .init('bg') + .load(data); + +console.log(i18n.getLanguage('bg').getTranslate('title')); +console.log(__('title')); +``` + +### Пример 2 + +```javascript +const data = { + title: "Example Application" +}; +const locale = "bg"; + +const i18n = I18nService + .init(locale) + .load(data, locale); + +console.log(i18n.getLanguage(locale).getTranslate('title')); +console.log(__('title')); +``` + +### Пример 3 + +```javascript +const sheet = SpreadsheetApp + .getActiveSpreadsheet() + .getSheetByName('I18n'); + +const i18n = I18nService + .init('bg') + .load(sheet); + +console.log(i18n.getLanguage('bg').getTranslate('title')); +console.log(__('title')); +``` + + +## Задачи + +- [ ] Добавете в метода `I18n.load()` възможността за зареждане на преводи `json` чрез url-връзка. +- [ ] Използвайте [`CacheService`](https://developers.google.com/apps-script/reference/cache) за съхранение на езика. + + +## Принос + +Моля, прочетете [CONTRIBUTING.md](CONTRIBUTING.md) за подробности относно това как да допринесете за този проект. + + +## История на промените + +Моля, направете справка с [CHANGELOG.md](CHANGELOG.md) за подробен списък на промените и актуализациите. + + +## Лиценз + +Този проект е лицензиран съгласно файла [LICENSE.md](LICENSE.md). \ No newline at end of file diff --git a/docs/bg/SECURITY.md b/docs/bg/SECURITY.md new file mode 100644 index 0000000..38876aa --- /dev/null +++ b/docs/bg/SECURITY.md @@ -0,0 +1,10 @@ +
+ Български + Deutsch + English + Русский + Українська +
+ + +# Докладване на проблем със сигурността diff --git a/docs/de/CHANGELOG.md b/docs/de/CHANGELOG.md new file mode 100644 index 0000000..a159ff2 --- /dev/null +++ b/docs/de/CHANGELOG.md @@ -0,0 +1,24 @@ +
+ Български + Deutsch + English + Русский + Українська +
+ + +# Changelog + + +## [1.1.2] - 2024-08-16 + +### Erweiterungen +* Verbesserte JSDoc-Dokumentation für mehr Klarheit und Detailgenauigkeit. + +### Korrekturen +* Verschiedene Bugs und Probleme wurden behoben. + + +## [1.0.0] - 2023-10-29 + +* Erste Veröffentlichung. \ No newline at end of file diff --git a/docs/de/CONTRIBUTING.md b/docs/de/CONTRIBUTING.md new file mode 100644 index 0000000..331dd2c --- /dev/null +++ b/docs/de/CONTRIBUTING.md @@ -0,0 +1,10 @@ +
+ Български + Deutsch + English + Русский + Українська +
+ + +# Wie wird man ein Beitragszahler? diff --git a/docs/de/LICENSE.md b/docs/de/LICENSE.md new file mode 100644 index 0000000..67eafd6 --- /dev/null +++ b/docs/de/LICENSE.md @@ -0,0 +1,30 @@ +
+ Български + Deutsch + English + Русский + Українська +
+ + +# MIT License + +Copyright (c) 2023 Maksym Stoianov + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/docs/de/README.md b/docs/de/README.md new file mode 100644 index 0000000..5e194fa --- /dev/null +++ b/docs/de/README.md @@ -0,0 +1,108 @@ +
+ Български + Deutsch + English + Русский + Українська +
+ + +# I18nService + +
+ Release + License + clasp +
+ +**I18nService** ist ein Objekt, das die Arbeit mit Internationalisierung umsetzt. + + +## Installation + +1. Öffnen Sie Ihr Projekt im [Google Apps Script Dashboard](https://script.google.com/). +2. Kopieren Sie den Inhalt der Datei [i18n.js](../../src/i18n.js) und fügen Sie ihn in eine neue Datei in Ihrem Google Apps Script-Projekt ein. + + +## Verwendung + +### Beispiel 1 + +```javascript +const data = { + "bg": { + title: "Тестово приложение" + }, + "de": { + title: "Testanwendung" + }, + "en": { + title: "Example Application" + }, + "ru": { + title: "Тестовое приложение" + }, + "uk": { + title: "Тестовий застосунок" + } +}; + +const i18n = I18nService + .init('de') + .load(data); + +console.log(i18n.getLanguage('de').getTranslate('title')); +console.log(__('title')); +``` + +### Beispiel 2 + +```javascript +const data = { + title: "Example Application" +}; +const locale = "de"; + +const i18n = I18nService + .init(locale) + .load(data, locale); + +console.log(i18n.getLanguage(locale).getTranslate('title')); +console.log(__('title')); +``` + +### Beispiel 3 + +```javascript +const sheet = SpreadsheetApp + .getActiveSpreadsheet() + .getSheetByName('I18n'); + +const i18n = I18nService + .init('de') + .load(sheet); + +console.log(i18n.getLanguage('de').getTranslate('title')); +console.log(__('title')); +``` + + +## Aufgaben + +- [ ] Fügen Sie die Möglichkeit hinzu, Übersetzungen im `json`-Format über einen URL-Link in die Methode `I18n.load()` zu laden. +- [ ] Verwenden Sie [`CacheService`](https://developers.google.com/apps-script/reference/cache) zur Speicherung der Sprache. + + +## Beitrag + +Bitte lesen Sie [CONTRIBUTING.md](CONTRIBUTING.md) für Details, wie Sie zu diesem Projekt beitragen können. + + +## Änderungshistorie + +Bitte lesen Sie [CHANGELOG.md](CHANGELOG.md) für eine detaillierte Liste der Änderungen und Aktualisierungen. + + +## Lizenz + +Dieses Projekt ist lizenziert unter der Datei [LICENSE.md](LICENSE.md). \ No newline at end of file diff --git a/docs/de/SECURITY.md b/docs/de/SECURITY.md new file mode 100644 index 0000000..8d35d6a --- /dev/null +++ b/docs/de/SECURITY.md @@ -0,0 +1,10 @@ +
+ Български + Deutsch + English + Русский + Українська +
+ + +# Ein Sicherheitsproblem melden diff --git a/docs/en/CHANGELOG.md b/docs/en/CHANGELOG.md new file mode 100644 index 0000000..7c7eeca --- /dev/null +++ b/docs/en/CHANGELOG.md @@ -0,0 +1,24 @@ +
+ Български + Deutsch + English + Русский + Українська +
+ + +# Changelog + + +## [1.1.2] - 2024-08-16 + +### Enhancements +* Enhanced JSDoc documentation for improved clarity and detail. + +### Fixes +* Resolved various bugs and issues. + + +## [1.0.0] - 2023-10-29 + +* Initial release. \ No newline at end of file diff --git a/docs/en/CONTRIBUTING.md b/docs/en/CONTRIBUTING.md new file mode 100644 index 0000000..54d954b --- /dev/null +++ b/docs/en/CONTRIBUTING.md @@ -0,0 +1,10 @@ +
+ Български + Deutsch + English + Русский + Українська +
+ + +# How to become a contributor? diff --git a/docs/en/LICENSE.md b/docs/en/LICENSE.md new file mode 100644 index 0000000..6ead6fa --- /dev/null +++ b/docs/en/LICENSE.md @@ -0,0 +1,30 @@ +
+ Български + Deutsch + English + Русский + Українська +
+ + +# MIT License + +Copyright (c) 2023 Maksym Stoianov + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/docs/en/README.md b/docs/en/README.md new file mode 100644 index 0000000..319c756 --- /dev/null +++ b/docs/en/README.md @@ -0,0 +1,108 @@ +
+ Български + Deutsch + English + Русский + Українська +
+ + +# I18nService + +
+ Release + License + clasp +
+ +**I18nService** is an object that implements internationalization functionality. + + +## Installation + +1. Open your project in the [Google Apps Script Dashboard](https://script.google.com/). +2. Copy the contents of the [i18n.js](../../src/i18n.js) file and paste it into a new file in your Google Apps Script project. + + +## Usage + +### Example 1 + +```javascript +const data = { + "bg": { + title: "Тестово приложение" + }, + "de": { + title: "Testanwendung" + }, + "en": { + title: "Example Application" + }, + "ru": { + title: "Тестовое приложение" + }, + "uk": { + title: "Тестовий застосунок" + } +}; + +const i18n = I18nService + .init('ru') + .load(data); + +console.log(i18n.getLanguage('en').getTranslate('title')); +console.log(__('title')); +``` + +### Example 2 + +```javascript +const data = { + title: "Example Application" +}; +const locale = "en"; + +const i18n = I18nService + .init(locale) + .load(data, locale); + +console.log(i18n.getLanguage(locale).getTranslate('title')); +console.log(__('title')); +``` + +### Example 3 + +```javascript +const sheet = SpreadsheetApp + .getActiveSpreadsheet() + .getSheetByName('I18n'); + +const i18n = I18nService + .init('en') + .load(sheet); + +console.log(i18n.getLanguage('en').getTranslate('title')); +console.log(__('title')); +``` + + +## Tasks + +- [ ] Add the ability to load translations in `json` format via url-link to the `I18n.load()` method. +- [ ] Use [`CacheService`](https://developers.google.com/apps-script/reference/cache) for language storage. + + +## Contribution + +Please read [CONTRIBUTING.md](CONTRIBUTING.md) for details on how to contribute to this project. + + +## Change History + +Please refer to [CHANGELOG.md](CHANGELOG.md) for a detailed list of changes and updates. + + +## License + +This project is licensed under the [LICENSE.md](LICENSE.md) file. \ No newline at end of file diff --git a/docs/en/SECURITY.md b/docs/en/SECURITY.md new file mode 100644 index 0000000..4deb32b --- /dev/null +++ b/docs/en/SECURITY.md @@ -0,0 +1,10 @@ +
+ Български + Deutsch + English + Русский + Українська +
+ + +# Report a security issue diff --git a/docs/ru/CHANGELOG.md b/docs/ru/CHANGELOG.md new file mode 100644 index 0000000..e35a5aa --- /dev/null +++ b/docs/ru/CHANGELOG.md @@ -0,0 +1,24 @@ +
+ Български + Deutsch + English + Русский + Українська +
+ + +# Changelog + + +## [1.1.2] - 2024-08-16 + +### Улучшения. +* Улучшена документация JSDoc для повышения ясности и детализации. + +### Исправления. +* Устранены различные ошибки и проблемы. + + +## [1.0.0] - 2023-10-29 + +* Первоначальный выпуск. \ No newline at end of file diff --git a/docs/ru/CONTRIBUTING.md b/docs/ru/CONTRIBUTING.md new file mode 100644 index 0000000..ebdc32e --- /dev/null +++ b/docs/ru/CONTRIBUTING.md @@ -0,0 +1,10 @@ +
+ Български + Deutsch + English + Русский + Українська +
+ + +# Как стать участником? diff --git a/docs/ru/LICENSE.md b/docs/ru/LICENSE.md new file mode 100644 index 0000000..708f6f1 --- /dev/null +++ b/docs/ru/LICENSE.md @@ -0,0 +1,30 @@ +
+ Български + Deutsch + English + Русский + Українська +
+ + +# MIT License + +Copyright (c) 2023 Maksym Stoianov + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/docs/ru/README.md b/docs/ru/README.md new file mode 100644 index 0000000..0820603 --- /dev/null +++ b/docs/ru/README.md @@ -0,0 +1,108 @@ +
+ Български + Deutsch + English + Русский + Українська +
+ + +# I18nService + +
+ Release + License + clasp +
+ +**I18nService** - представляет собой объект реализующий работу интернационализации. + + +## Установка + +1. Откройте свой проект в [Google Apps Script Dashboard](https://script.google.com/). +2. Скопируйте содержимое файла [i18n.js](../../src/i18n.js) и вставьте его в новый файл в вашем проекте Google Apps Script. + + +## Использование + +### Пример 1 + +```javascript +const data = { + "bg": { + title: "Тестово приложение" + }, + "de": { + title: "Testanwendung" + }, + "en": { + title: "Example Application" + }, + "ru": { + title: "Тестовое приложение" + }, + "uk": { + title: "Тестовий застосунок" + } +}; + +const i18n = I18nService + .init('ru') + .load(data); + +console.log(i18n.getLanguage('ru').getTranslate('title')); +console.log(__('title')); +``` + +### Пример 2 + +```javascript +const data = { + title: "Example Application" +}; +const locale = "ru"; + +const i18n = I18nService + .init(locale) + .load(data, locale); + +console.log(i18n.getLanguage(locale).getTranslate('title')); +console.log(__('title')); +``` + +### Пример 3 + +```javascript +const sheet = SpreadsheetApp + .getActiveSpreadsheet() + .getSheetByName('I18n'); + +const i18n = I18nService + .init('ru') + .load(sheet); + +console.log(i18n.getLanguage('ru').getTranslate('title')); +console.log(__('title')); +``` + + +## Задачи + +- [ ] В метод `I18n.load()` добавить возможность загружать переводы `json` по url-ссылке. +- [ ] Использовать [`CacheService`](https://developers.google.com/apps-script/reference/cache) для хранения языка. + + +## Вклад + +Пожалуйста, прочитайте [CONTRIBUTING.md](CONTRIBUTING.md) для получения подробной информации о том, как внести вклад в этот проект. + + +## История изменений + +Для получения подробного списка изменений и обновлений, пожалуйста, обратитесь к файлу [CHANGELOG.md](CHANGELOG.md). + + +## Лицензия + +Этот проект лицензируется в соответствии с файлом [LICENSE.md](LICENSE.md). \ No newline at end of file diff --git a/docs/ru/SECURITY.md b/docs/ru/SECURITY.md new file mode 100644 index 0000000..d3563b2 --- /dev/null +++ b/docs/ru/SECURITY.md @@ -0,0 +1,10 @@ +
+ Български + Deutsch + English + Русский + Українська +
+ + +# Сообщить о проблеме с безопасностью diff --git a/docs/uk/CHANGELOG.md b/docs/uk/CHANGELOG.md new file mode 100644 index 0000000..8f26a74 --- /dev/null +++ b/docs/uk/CHANGELOG.md @@ -0,0 +1,24 @@ +
+ Български + Deutsch + English + Русский + Українська +
+ + +# Журнал змін + + +## [1.1.2] - 2024-08-16 + +### Вдосконалення +* Покращено документацію JSDoc для більшої ясності та деталізації. + +### Виправлено +* Виправлено різноманітні вади та проблеми. + + +## [1.0.0] - 2023-10-29 + +* Початковий випуск. \ No newline at end of file diff --git a/docs/uk/CONTRIBUTING.md b/docs/uk/CONTRIBUTING.md new file mode 100644 index 0000000..52ffe64 --- /dev/null +++ b/docs/uk/CONTRIBUTING.md @@ -0,0 +1,10 @@ +
+ Български + Deutsch + English + Русский + Українська +
+ + +# Як стати учасником? diff --git a/docs/uk/LICENSE.md b/docs/uk/LICENSE.md new file mode 100644 index 0000000..8622ea0 --- /dev/null +++ b/docs/uk/LICENSE.md @@ -0,0 +1,30 @@ +
+ Български + Deutsch + English + Русский + Українська +
+ + +# MIT License + +Copyright (c) 2023 Maksym Stoianov + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/docs/uk/README.md b/docs/uk/README.md new file mode 100644 index 0000000..de1e9f5 --- /dev/null +++ b/docs/uk/README.md @@ -0,0 +1,108 @@ +
+ Български + Deutsch + English + Русский + Українська +
+ + +# I18nService + +
+ Release + License + clasp +
+ +**I18nService** - це об'єкт, що реалізує роботу з інтернаціоналізацією. + + +## Встановлення + +1. Відкрийте свій проект у [Google Apps Script Dashboard](https://script.google.com/). +2. Скопіюйте вміст файлу [i18n.js](../../src/i18n.js) і вставте його у новий файл у вашому проекті Google Apps Script. + + +## Використання + +### Приклад 1 + +```javascript +const data = { + "bg": { + title: "Тестово приложение" + }, + "de": { + title: "Testanwendung" + }, + "en": { + title: "Example Application" + }, + "ru": { + title: "Тестовое приложение" + }, + "uk": { + title: "Тестовий застосунок" + } +}; + +const i18n = I18nService + .init('uk') + .load(data); + +console.log(i18n.getLanguage('uk').getTranslate('title')); +console.log(__('title')); +``` + +### Приклад 2 + +```javascript +const data = { + title: "Example Application" +}; +const locale = "uk"; + +const i18n = I18nService + .init(locale) + .load(data, locale); + +console.log(i18n.getLanguage(locale).getTranslate('title')); +console.log(__('title')); +``` + +### Приклад 3 + +```javascript +const sheet = SpreadsheetApp + .getActiveSpreadsheet() + .getSheetByName('I18n'); + +const i18n = I18nService + .init('uk') + .load(sheet); + +console.log(i18n.getLanguage('uk').getTranslate('title')); +console.log(__('title')); +``` + + +## Завдання + +- [ ] Додати до методу `I18n.load()` можливість завантажувати переклади `json` за url-посиланням. +- [ ] Використовувати [`CacheService`](https://developers.google.com/apps-script/reference/cache) для зберігання мови. + + +## Внесок + +Будь ласка, прочитайте [CONTRIBUTING.md](CONTRIBUTING.md) для отримання докладної інформації про те, як зробити внесок у цей проект. + + +## Історія змін + +Для отримання докладного списку змін і оновлень, будь ласка, зверніться до файлу [CHANGELOG.md](CHANGELOG.md). + + +## Ліцензія + +Цей проект ліцензується відповідно до файлу [LICENSE.md](LICENSE.md). \ No newline at end of file diff --git a/docs/uk/SECURITY.md b/docs/uk/SECURITY.md new file mode 100644 index 0000000..f950a0f --- /dev/null +++ b/docs/uk/SECURITY.md @@ -0,0 +1,10 @@ +
+ Български + Deutsch + English + Русский + Українська +
+ + +# Повідомити про проблему з безпекою diff --git a/src/appsscript.json b/src/appsscript.json new file mode 100644 index 0000000..bcb1a1d --- /dev/null +++ b/src/appsscript.json @@ -0,0 +1,4 @@ +{ + "exceptionLogging": "STACKDRIVER", + "runtimeVersion": "V8" +} \ No newline at end of file diff --git a/src/i18n.js b/src/i18n.js new file mode 100644 index 0000000..b22316c --- /dev/null +++ b/src/i18n.js @@ -0,0 +1,686 @@ +/** + * MIT License + * + * Copyright (c) 2023 Maksym Stoianov + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + + + +/** + * `I18nService` - представляет собой объект реализующий работу интернационализации. + * @class I18nService + * @namespace I18nService + * @version 1.1.2 + * @author Maksym Stoianov + * @license MIT + * @tutorial https://maksymstoianov.com/ + * @see [GitHub](https://github.com/MaksymStoianov/I18nService) + * + * @todo В метод `I18n.load()` добавить возможность загружать переводы `json` по url-ссылке. + * @todo Определять метаданные, например: + * "@metadata": { + * "authors": ['Maksym Stoianov '], + * "updated": "2024-04-19", + * "locale": "en", + * }; + */ +class I18nService { + + /** + * Статический метод для инициализации сервиса. + * @param {string} [locale] + * @param {object} [options = {}] + * @param {integer} [options.cache = 0] Определяет время хранения языка в [`CacheService`](https://developers.google.com/apps-script/reference/cache/cache-service) в секундах. + * @param {boolean} [options.translate = false] Включает автоматический перевод через [`LanguageApp`](https://developers.google.com/apps-script/reference/language/language-app). + */ + static init(...args) { + return Reflect.construct(this.I18n, args); + } + + + + /** + * Создает и возвращает экземпляр класса [`I18n`](#). + * @return {I18nService.I18n} + */ + static newI18n(...args) { + return Reflect.construct(this.I18n, args); + } + + + + /** + * Создает и возвращает экземпляр класса [`Language`](#). + * @return {I18nService.Language} + */ + static newLanguage(...args) { + return Reflect.construct(this.Language, args); + } + + + + /** + * Cтатический метод, который используется для определения правильной формы множественного числа слова. + * @param {number} n + * @param {string[]} forms + * @return {string} + */ + static pluralize(n, forms) { + return forms[n % 10 === 1 && n % 100 !== 11 ? 0 : n % 10 >= 2 && n % 10 <= 4 && (n % 100 < 10 || n % 100 >= 20) ? 1 : 2]; + } + + + + /** + * Возвращает `true`, если объект является [`I18n`](#); иначе, `false`. + * @param {*} input Значение для проверки. + * @return {boolean} + */ + static isI18n(input) { + if (!arguments.length) + throw new Error(`The parameters () don't match any method signature for ${this.name}.isI18n.`); + + return (input instanceof this.I18n); + } + + + + /** + * Возвращает `true`, если объект является [`Language`](#); иначе, `false`. + * @param {*} input Значение для проверки. + * @return {boolean} + */ + static isLanguage(input) { + if (!arguments.length) + throw new Error(`The parameters () don't match any method signature for ${this.name}.isLanguage.`); + + return (input instanceof this.Language); + } + + + + /** + * + * @param {*} input Значение для проверки. + * @return {boolean} + */ + static isValidLocaleName(input) { + if (!arguments.length) + throw new Error(`The parameters () don't match any method signature for ${this.name}.isValidLocaleName.`); + + return ( + typeof input === 'string' && + input.trim().length - 2 >= 0 + ); + } + + + + /** + * + * @param {*} input Значение для проверки. + * @return {boolean} + */ + static isValidLabelName(input) { + if (!arguments.length) + throw new Error(`The parameters () don't match any method signature for ${this.name}.isValidLabelName.`); + + return ( + typeof input === 'string' && + input.trim().length - 1 >= 0 + ); + } + + + + /** + * + * @param {*} input Значение для проверки. + * @return {boolean} + */ + static isValidTranslateName(input) { + if (!arguments.length) + throw new Error(`The parameters () don't match any method signature for ${this.name}.isValidTranslateName.`); + + return (typeof input === 'string'); + } + + + + constructor() { + throw new Error(`${this.constructor.name} is not a constructor.`); + } + +} + + + + + +/** + * Конструктор 'I18n' - представляет собой объект для работы с ... + * @class I18n + * @memberof I18nService + * @version 1.1.2 + */ +I18nService.I18n = class I18n { + + /** + * @param {string} [locale] + * @param {object} [options = {}] + * @param {integer} [options.cache = 0] todo Использовать кеш для хранения языка. + * @param {boolean} [options.translate = false] Автоматически переводить с помощью Google Переводчика. + */ + constructor(locale, options = {}) { + + /** + * @type {string} + */ + this.locale = null; + + + /** + * @private + * @type {Object} + */ + Object.defineProperty(this, '_values', { + configurable: true, + enumerable: false, + writable: true, + value: {} + }); + + + if (locale) + this.setLocale(locale); + + if (typeof options.translate === 'boolean') + this.translate = options.translate; + + + /** + * Получает перевод. + * @param {string} label + * @param {string} [locale] + * @return {string} + */ + globalThis.__ = (label, locale) => this + .getLanguage(locale ?? this.locale) + .getTranslate(label, this.translate); + + } + + + + /** + * Загружает языки. + */ + /** + * @overload + * @param {I18nService.I18n} i18n Экземпляр класса [`I18n`](#). + * @return {I18nService.I18n} + */ + /** + * @overload + * @param {I18nService.Language} language Экземпляр класса [`Language`](#). + * @return {I18nService.I18n} + */ + /** + * @overload + * @param {string} spreadsheetId + * @param {string} sheetName + * @return {I18nService.I18n} + */ + /** + * @overload + * @param {SpreadsheetApp.Spreadsheet} spreadsheet Экземпляр класса [`Sheet`](https://developers.google.com/apps-script/reference/spreadsheet/spreadsheet). + * @param {string} sheetName + * @return {I18nService.I18n} + */ + /** + * @overload + * @param {SpreadsheetApp.Sheet} sheet Экземпляр класса [`Sheet`](https://developers.google.com/apps-script/reference/spreadsheet/sheet). + * @return {I18nService.I18n} + */ + /** + * @overload + * @param {object} json + * @param {string} locale + * @return {I18nService.I18n} + */ + /** + * @overload + * @param {object} json + * @return {I18nService.I18n} + */ + load(...args) { + if (!arguments.length) + throw new Error(`Параметры () не соответствуют сигнатуре метода ${this.constructor.name}.load.`); + + let data = {}; + let sheet = null; + + + /** + * @param {string} locale + * @param {Object} translates + */ + const setTranslates_ = (locale, translates) => { + if (!I18nService.isValidLocaleName(locale)) return false; + + // Метка перевода + locale = locale.trim().toLowerCase(); + + if (!data[locale]) + data[locale] = {}; + + // todo translates === url + + for (let label in translates) { + // Метка перевода + label = (item => typeof item === 'string' && item.length > 0 ? item : null)(label); + + if (!I18nService.isValidLabelName(label)) continue; + + let translate = (item => typeof item === 'string' && item.length >= 0 ? item : null)(translates[label]); + + if (!I18nService.isValidTranslateName(translate)) continue; + + // Устанавливаем текст перевода + data[locale][label] = translate; + } + }; + + + // Аргументы: I18n + if (args.length === 1 && I18nService.isI18n(args[0]) && Object.isObject(args[0]?._values)) + this._values = args[0]._values; + + // Аргументы: Language + else if (args.length === 1 && I18nService.isLanguage(args[0]) && Object.isObject(args[0]?._values) && typeof args[0]?.locale === 'string') + this._values[args[0].locale] = args[0]; + + // Аргументы: spreadsheetId, sheetName + else if (args.length === 2 && typeof args[0] === 'string' && args[0].length && typeof args[1] === 'string' && args[1].length) + sheet = (ss => ss.getSheetByName(args[1]) ?? ss.insertSheet(args[1]))(SpreadsheetApp.openById(args[0])); + + // Аргументы: spreadsheet, sheetName + else if (args.length === 2 && SpreadsheetApp.isSpreadsheet(args[0]) && typeof args[1] === 'string' && args[1].length) + sheet = args[0].getSheetByName(args[1]) ?? args[0].insertSheet(args[1]); + + // Аргументы: sheet + else if (args.length === 1 && SpreadsheetApp.isSheet(args[0])) + sheet = args[0]; + + // Аргументы: json, locale + else if (args.length === 2 && Object.isObject(args[0]) && typeof args[1] === 'string') + setTranslates_(args[1], args[0]); + + // Аргументы: json + else if (args.length === 1 && Object.isObject(args[0])) + for (const locale in args[0]) + setTranslates_(locale, args[0][locale]); + + else throw new Error(`Недопустимые аргументы: невозможно определить правильную перегрузку статического метода ${this.constructor.name}.load.`); + + + if (sheet) { + const [headers, ...values] = sheet + .getDataRange() + .getDisplayValues(); + + let json = headers + .slice(1) + .map(item => [item, {}]); + + for (const [label, ...translates] of values) + for (const [i] of json.entries()) + json[i][1][label] = translates[i]; + + json = Object.fromEntries(json); + + for (const locale in json) + setTranslates_(locale, json[locale]); + } + + for (const locale in data) { + const translates = data[locale]; + + if (!this._values[locale]) + this._values[locale] = I18nService.newLanguage(locale, translates); + } + + return this; + } + + + + /** + * Устанавливает локаль для сервиса. + * @param {string} locale Код локали, который нужно установить. + * @return {I18nService.I18n} Возвращает текущий экземпляр для цепочки вызовов. + */ + setLocale(locale) { + if (!arguments.length) + throw new Error(`Параметры () не соответствуют сигнатуре метода ${this.constructor.name}.setLocale.`); + + if (!I18nService.isValidLocaleName(locale)) + throw new Error(`Параметры (${typeof locale}) не соответствуют сигнатуре метода ${this.constructor.name}.setLocale.`); + + this.locale = locale.trim().toLowerCase(); + + this._values[this.locale] = I18nService.newLanguage(locale); + + return this; + } + + + + /** + * Возвращает установленную локаль. + * @return {string} Возвращает текущую локаль или `null`, если локаль не установлена. + */ + getLocale() { + return this.locale ?? null; + } + + + + /** + * Получает экземпляр класса [`Language`](#) на основе указанной локали. + * @param {string} locale Код локали для получения языка. + * @return {I18nService.Language} Экземпляр класса [`Language`](#) или `null`. + */ + getLanguage(locale) { + if (!arguments.length) + throw new Error(`Параметры () не соответствуют сигнатуре статического метода ${this.constructor.name}.getLanguage.`); + + if (!I18nService.isValidLocaleName(locale)) + throw new Error(`Параметры (${typeof locale}) не соответствуют сигнатуре метода ${this.constructor.name}.getLanguage.`); + + return this._values[locale] ?? (this._values[locale] = I18nService.newLanguage(locale)); + } + + + + /** + * @param {string} locale + * @return {boolean} + */ + hasLanguage(locale) { + return !!this.values[locale]; + } + + + + /** + * Вызывается при преобразовании объекта в соответствующее примитивное значение. + * @param {string} hint Строковый аргумент, который передаёт желаемый тип примитива: `string`, `number` или `default`. + * @return {string} + */ + [Symbol.toPrimitive](hint) { + if (hint !== 'string') + return null; + + return this.constructor.name; + } + + + + /** + * Возвращает значение текущего объекта. + * @return {string} + */ + valueOf() { + return (this[Symbol.toPrimitive] ? this[Symbol.toPrimitive]() : this.constructor.name); + } + + + + /** + * Геттер для получения строки, представляющей тег объекта. + * @return {string} Имя класса текущего объекта, используемое в `Object.prototype.toString`. + */ + get [Symbol.toStringTag]() { + return this.constructor.name; + } + + + + /** + * Возвращает строку, представляющую объект. + * @return {string} + */ + toString() { + return (this[Symbol.toPrimitive] ? this[Symbol.toPrimitive]('string') : this.constructor.name); + } + +}; + + + + + +/** + * Конструктор 'Language' - представляет собой объект для работы с ... + * @class Language + * @memberof I18nService + * @version 1.1.2 + */ +I18nService.Language = class Language { + + /** + * @param {string} locale + * @param {Object} [values = {}] + */ + constructor(locale, values = {}) { + + /** + * @type {string} + */ + this.locale = locale; + + + /** + * @type {Object} + */ + this.values = {}; + + + if (Object.isObject(values)) { + for (const label in values) { + this.addTranslate(label, values[label]); + } + } + + } + + + + /** + * Устанавливает локаль для сервиса. + * @param {string} locale Код локали, который нужно установить. + * @return {I18nService.Language} Возвращает текущий экземпляр для цепочки вызовов. + */ + setLocale(locale) { + if (!arguments.length) + throw new Error(`Параметры () не соответствуют сигнатуре метода ${this.constructor.name}.setLocale.`); + + if (!I18nService.isValidLocaleName(locale)) + throw new Error(`Параметры (${typeof locale}) не соответствуют сигнатуре метода ${this.constructor.name}.setLocale.`); + + this.locale = locale.trim(); + + return this; + } + + + + /** + * Возвращает установленную локаль. + * @return {string} Возвращает текущую локаль или `null`, если локаль не установлена. + */ + getLocale() { + return this.locale ?? null; + } + + + + /** + * @param {string} label + * @param {string} translate + * @return {I18nService.Language} Возвращает текущий экземпляр для цепочки вызовов. + */ + addTranslate(label, translate) { + if (!arguments.length) + throw new Error(`Параметры () не соответствуют сигнатуре метода ${this.constructor.name}.addTranslate.`); + + if (!I18nService.isValidLabelName(label)) + throw new Error(`Параметры (${typeof label}) не соответствуют сигнатуре метода ${this.constructor.name}.addTranslate.`); + + if (!I18nService.isValidTranslateName(translate)) + throw new Error(`Параметры (${typeof label}, ${typeof translate}) не соответствуют сигнатуре метода ${this.constructor.name}.addTranslate.`); + + this.values[label.toLowerCase()] = translate; + + return this; + } + + + + /** + * @param {string} label + * @param {boolean} [isTranslate = false] + * @return {string} + */ + getTranslate(label, isTranslate = false) { + if (!arguments.length) + throw new Error(`Параметры () не соответствуют сигнатуре метода ${this.constructor.name}.getTranslate.`); + + if (!I18nService.isValidLabelName(label)) + throw new Error(`Параметры (${typeof label}) не соответствуют сигнатуре метода ${this.constructor.name}.getTranslate.`); + + if (typeof isTranslate !== 'boolean') + throw new Error(`Параметры (${typeof label}, ${typeof isTranslate}) не соответствуют сигнатуре метода ${this.constructor.name}.getTranslate.`); + + const locale = this.getLocale(); + + if (!locale) + throw new Error(`Недопустимый аргумент locale.`); + + return this.values[label.toLowerCase()] ?? (isTranslate === true ? LanguageApp.translate(label, 'auto', locale) : label); + } + + + + /** + * Вызывается при преобразовании объекта в соответствующее примитивное значение. + * @param {string} hint Строковый аргумент, который передаёт желаемый тип примитива: `string`, `number` или `default`. + * @return {string} + */ + [Symbol.toPrimitive](hint) { + if (hint !== 'string') + return null; + + return this.constructor.name; + } + + + + /** + * Возвращает значение текущего объекта. + * @return {string} + */ + valueOf() { + return (this[Symbol.toPrimitive] ? this[Symbol.toPrimitive]() : this.constructor.name); + } + + + + /** + * Геттер для получения строки, представляющей тег объекта. + * @return {string} Имя класса текущего объекта, используемое в `Object.prototype.toString`. + */ + get [Symbol.toStringTag]() { + return this.constructor.name; + } + + + + /** + * Возвращает строку, представляющую объект. + * @return {string} + */ + toString() { + return (this[Symbol.toPrimitive] ? this[Symbol.toPrimitive]('string') : this.constructor.name); + } + +}; + + + + + +if (typeof Object.isObject !== 'function') { + /** + * Возвращает `true`, если объект является [`Object`](#); иначе, `false`. + * @param {*} input Объект для проверки. + * @return {boolean} + */ + Object.isObject = input => ( + input === Object(input) && + !Array.isArray(input) + ); +} + + + + + +if (typeof SpreadsheetApp.isSpreadsheet !== 'function') { + /** + * Возвращает `true`, если объект является [`Spreadsheet`](https://developers.google.com/apps-script/reference/spreadsheet/spreadsheet); иначе, `false`. + * @param {*} input Объект для проверки. + * @return {boolean} + */ + SpreadsheetApp.isSpreadsheet = input => ( + input === Object(input) && + !Array.isArray(input) && + input.toString() === 'Spreadsheet' + ); +} + + + + + +if (typeof SpreadsheetApp.isSheet !== 'function') { + /** + * Возвращает `true`, если объект является [`Sheet`](https://developers.google.com/apps-script/reference/spreadsheet/sheet); иначе, `false`. + * @param {*} input Объект для проверки. + * @return {boolean} + */ + SpreadsheetApp.isSheet = input => ( + input === Object(input) && + !Array.isArray(input) && + input.toString() === 'Sheet' + ); +} diff --git a/src/tests.js b/src/tests.js new file mode 100644 index 0000000..e69de29