├── .eslintrc
├── .github
└── workflows
│ └── test.yml
├── .gitignore
├── .npmignore
├── .prettierrc
├── LICENSE.md
├── README.md
├── __tests__
├── __mocks__
│ └── lang
│ │ ├── de.json
│ │ ├── en.json
│ │ ├── en
│ │ ├── auth.php
│ │ ├── custom.php
│ │ ├── random.php
│ │ └── validation.php
│ │ ├── fr
│ │ ├── auth.php
│ │ └── random.php
│ │ ├── it
│ │ └── auth.php
│ │ ├── nl.json
│ │ ├── php_en.json
│ │ ├── php_fr.json
│ │ ├── php_it.json
│ │ └── uk.json
├── jest-setup.ts
├── plugin
│ ├── helper.test.ts
│ ├── locale.test.ts
│ └── parser.test.ts
├── provider.test.tsx
└── utils
│ ├── pluralization.test.ts
│ ├── recognizer.test.ts
│ ├── replacer.test.ts
│ └── resolver.test.ts
├── babel.config.json
├── jest.config.json
├── package-lock.json
├── package.json
├── src
├── @types
│ └── php-array-reader.d.ts
├── context.ts
├── contrib
│ └── get-plural-index.ts
├── hook.ts
├── index.ts
├── interfaces
│ ├── context.ts
│ ├── default-options.ts
│ ├── i18n-provider-props.ts
│ ├── locale-file.ts
│ ├── options-provider.ts
│ ├── options.ts
│ └── replacements.ts
├── plugin
│ ├── helper.ts
│ ├── key-type.ts
│ ├── locale.ts
│ └── parser.ts
├── provider.ts
├── utils
│ ├── pluralization.ts
│ ├── recognizer.ts
│ ├── replacer.ts
│ └── resolver.ts
└── vite.ts
├── tsconfig.client.json
├── tsconfig.commonjs.json
├── tsconfig.json
├── tsconfig.plugin.json
└── vite.js
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "browser": true,
4 | "jest": true
5 | },
6 | "extends": [
7 | "airbnb-typescript",
8 | "plugin:@typescript-eslint/recommended",
9 | "plugin:jest/recommended",
10 | "plugin:prettier/recommended"
11 | ],
12 | "plugins": ["import", "react", "@typescript-eslint", "jest"],
13 | "parserOptions": {
14 | "ecmaFeatures": {
15 | "jsx": true
16 | },
17 | "ecmaVersion": 2018,
18 | "sourceType": "module",
19 | "project": "tsconfig.json"
20 | },
21 | "rules": {
22 | "@typescript-eslint/no-unused-vars": "off",
23 | "@typescript-eslint/no-use-before-define": "off",
24 | "@typescript-eslint/no-shadow": "off",
25 | "consistent-return": "off",
26 | "import/no-extraneous-dependencies": "off",
27 | "import/prefer-default-export": "off",
28 | "no-nested-ternary": "off",
29 | "no-param-reassign": "off",
30 | "no-promise-executor-return": "off",
31 | "prefer-destructuring": "off"
32 | }
33 | }
--------------------------------------------------------------------------------
/.github/workflows/test.yml:
--------------------------------------------------------------------------------
1 | name: Testing
2 |
3 | on:
4 | push:
5 | branches: [ "master" ]
6 | pull_request:
7 | branches: [ "master" ]
8 |
9 | jobs:
10 | build:
11 |
12 | runs-on: ubuntu-latest
13 |
14 | strategy:
15 | matrix:
16 | node-version: [ 16.x, 18.x]
17 | # See supported Node.js release schedule at https://nodejs.org/en/about/releases/
18 |
19 | steps:
20 | - uses: actions/checkout@v3
21 | - name: Use Node.js ${{ matrix.node-version }}
22 | uses: actions/setup-node@v3
23 | with:
24 | node-version: ${{ matrix.node-version }}
25 | cache: 'npm'
26 | - run: npm install
27 | - run: npm test
28 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /.fleet
2 | /.idea
3 | /.vscode
4 | /node_modules
5 | /coverage
6 | /dist
7 | npm-debug.log*
8 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | coverage
2 | dist
3 | __tests__
4 | /.fleet
5 | /.idea
6 | /.vscode
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "singleQuote": true,
3 | "printWidth": 120,
4 | "trailingComma": "none"
5 | }
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) Eugene Meles xmelesx@gmail.com
4 |
5 | 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:
6 |
7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
8 |
9 | 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.
10 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 | Laravel React i18n
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 | laravel-react-i18n is a React plugin that allows to connect your Laravel Framework translation
14 | files with React . It uses the same logic used on Laravel Localization .
15 |
16 |
17 | ## Demo
18 | [laravel-react-i18n-playground](https://github.com/EugeneMeles/laravel-react-i18n-playground)
19 |
20 | ## Requirements
21 | - Laravel >= 9
22 | - NodeJS >= 16
23 | - Vite
24 |
25 | ## Installation
26 |
27 | With [npm](https://www.npmjs.com):
28 | ```sh
29 | npm i laravel-react-i18n
30 | ```
31 |
32 | or with [yarn](https://yarnpkg.com):
33 | ```sh
34 | yarn add laravel-react-i18n
35 | ```
36 |
37 | ## Setup
38 |
39 |
40 | #### CSR (Client Side Rendering)
41 |
42 | `app.tsx:`
43 |
44 | ```tsx
45 | import './bootstrap';
46 | import '../css/app.css';
47 |
48 | import { createRoot } from 'react-dom/client';
49 | import { createInertiaApp } from '@inertiajs/react';
50 | import { resolvePageComponent } from 'laravel-vite-plugin/inertia-helpers';
51 | import { LaravelReactI18nProvider } from 'laravel-react-i18n';
52 |
53 | const appName = window.document.getElementsByTagName('title')[0]?.innerText || 'Laravel';
54 |
55 | createInertiaApp({
56 | title: (title) => `${title} - ${appName}`,
57 | resolve: (name) => resolvePageComponent(`./Pages/${name}.tsx`, import.meta.glob('./Pages/**/*.tsx')),
58 | setup({ el, App, props }) {
59 | const root = createRoot(el);
60 |
61 | root.render(
62 |
67 |
68 |
69 | );
70 | },
71 | progress: {
72 | color: '#4B5563',
73 | },
74 | });
75 |
76 | ```
77 |
78 | #### SSR (Server Side Rendering)
79 |
80 | `ssr.tsx:`
81 |
82 | ```tsx
83 | import ReactDOMServer from 'react-dom/server';
84 | import { createInertiaApp } from '@inertiajs/react';
85 | import createServer from '@inertiajs/react/server';
86 | import { resolvePageComponent } from 'laravel-vite-plugin/inertia-helpers';
87 | import route from '../../vendor/tightenco/ziggy/dist/index.m';
88 | import { LaravelReactI18nProvider } from 'laravel-react-i18n';
89 |
90 | const appName = 'Laravel';
91 |
92 | createServer((page) =>
93 | createInertiaApp({
94 | page,
95 | render: ReactDOMServer.renderToString,
96 | title: (title) => `${title} - ${appName}`,
97 | resolve: (name) => resolvePageComponent(`./Pages/${name}.tsx`, import.meta.glob('./Pages/**/*.tsx')),
98 | setup: ({ App, props }) => {
99 | global.route = (name, params, absolute) =>
100 | route(name, params, absolute, {
101 | // @ts-expect-error
102 | ...page.props.ziggy,
103 | // @ts-expect-error
104 | location: new URL(page.props.ziggy.location),
105 | });
106 |
107 | return (
108 |
113 |
114 |
115 | );
116 | },
117 | })
118 | );
119 |
120 | ```
121 |
122 | #### PHP Translations Available on React
123 |
124 | In order to load `php` translations, you can use this `Vite` plugin.
125 |
126 | `vite.config.js:`
127 |
128 | ```js
129 | import i18n from 'laravel-react-i18n/vite'; // <-- add this
130 |
131 | export default defineConfig({
132 | plugins: [
133 | laravel([
134 | 'resources/css/app.css',
135 | 'resources/js/app.js'
136 | ]),
137 | react(),
138 | i18n(), // <-- add this
139 | ],
140 | });
141 | ```
142 |
143 | > During the `npm run dev` execution time, the plugin will create some files like this `php_{lang}.json` on your lang folder.
144 | > And to avoid that to be commited to your code base, I suggest to your `.gitignore` this like:
145 |
146 | ```
147 | lang/php_*.json
148 | ```
149 |
150 | ## Usage
151 |
152 |
153 | ### Provider Options
154 |
155 | - `locale` *(optional)*: If not provided it will try to find from the `` tag or set `en`.
156 | - `fallbackLocale` *(optional)*: If the `locale` was not provided or is invalid, it will try reach for this `fallbackLocale` instead, default it will try to find from the `` tag or set `en`.
157 | - `files` *(required)*: The way to reach your language files.
158 |
159 | ```js
160 |
164 | ...
165 | ```
166 |
167 | ### Hook Response:
168 | ```tsx
169 | ...
170 | import { useLaravelReactI18n } from 'laravel-react-i18n';
171 |
172 | export default function Component() {
173 | const { t, tChoice, currentLocale, setLocale, getLocales, isLocale, loading } = useLaravelReactI18n();
174 | ...
175 | }
176 |
177 | ```
178 |
179 | ### `t(key: string, replacements?: {[key: string]: string | number})`
180 |
181 | The `t()` method can translate a given message.
182 |
183 | `lang/pt.json:`
184 | ```json
185 | {
186 | "Welcome!": "Bem-vindo!",
187 | "Welcome, :name!": "Bem-vindo, :name!"
188 | }
189 | ```
190 |
191 | `welcome.tsx:`
192 | ```tsx
193 | ...
194 | const { t } = useLaravelReactI18n();
195 |
196 | t('Welcome!'); // Bem-vindo!
197 | t('Welcome, :name!', { name: 'Francisco' }); // Bem-vindo Francisco!
198 | t('Welcome, :NAME!', { name: 'Francisco' }); // Bem-vindo FRANCISCO!
199 | t('Some untranslated'); // Some untranslated
200 | ...
201 | ```
202 |
203 | ### `tChoice(key: string, count: number, replacements?: {[key: string]: string | number})`
204 |
205 | The `tChoice()` method can translate a given message based on a count,
206 | there is also available an `trans_choice` alias, and a mixin called `$tChoice()`.
207 |
208 | `lang/pt.json:`
209 | ```json
210 | {
211 | "There is one apple|There are many apples": "Existe uma maça|Existe muitas maças",
212 | "{0} There are none|[1,19] There are some|[20,*] There are many": "Não tem|Tem algumas|Tem muitas",
213 | "{1} :count minute ago|[2,*] :count minutes ago": "{1} há :count minuto|[2,*] há :count minutos",
214 | }
215 | ```
216 |
217 | `choice.tsx:`
218 | ```tsx
219 | ...
220 | const { tChoice } = useLaravelReactI18n()
221 |
222 | tChoice('There is one apple|There are many apples', 1); // Existe uma maça
223 | tChoice('{0} There are none|[1,19] There are some|[20,*] There are many', 19); // Tem algumas
224 | tChoice('{1} :count minute ago|[2,*] :count minutes ago', 10); // Há 10 minutos.
225 | ...
226 | ```
227 |
228 | ### `currentLocale()`
229 |
230 | The `currentLocale()` returns the locale that is currently being used.
231 |
232 | ```tsx
233 | const { currentLocale } = useLaravelReactI18n()
234 |
235 | currentLocale(); // en
236 | ```
237 |
238 | ### `setLocale(locale: string)`
239 |
240 | The `setLocale()` can be used to change the locale during the runtime.
241 |
242 | ```tsx
243 | const { currentLocale, setLocale } = useLaravelReactI18n();
244 |
245 | function handler() {
246 | setLocale('it')
247 | }
248 |
249 | return (
250 |
251 |
Current locale: `{currentLocale()}`
252 | Change locale to `it`
253 |
254 | )
255 | ```
256 |
257 | ### `getLocales()`
258 |
259 | The `getLocales()` return string array with all locales available in folder `/lang/*`.
260 |
261 | ```text
262 | /lang/..
263 | de.json
264 | en.json
265 | nl.json
266 | uk.json
267 | ```
268 |
269 | `myLocales.tsx:`
270 | ```tsx
271 | const { getLocales } = useLaravelReactI18n();
272 |
273 | getLocales(); // ['de', 'en', 'nl', 'uk']
274 | ```
275 |
276 | ### `isLocale(locale: string)`
277 |
278 | The `isLocale()` method checks the locale is available in folder `/lang/*`.
279 |
280 | ```tsx
281 | const { isLocale } = useLaravelReactI18n();
282 |
283 | isLocale('uk'); // true
284 | isLocale('fr'); // false
285 | ```
286 |
287 | ### `loading`
288 |
289 | The `loading` show current loading state, only on client side where you change the locale.
290 |
291 | ```tsx
292 | const { loading, currentLocale, setLocale } = useLaravelReactI18n();
293 |
294 | function handler() {
295 | setLocale('it')
296 | }
297 |
298 | if (loading) return Loading...
;
299 |
300 | return (
301 |
302 |
Current locale: `{currentLocale()}`
303 | Change locale to `it`
304 |
305 | )
306 | ```
307 |
--------------------------------------------------------------------------------
/__tests__/__mocks__/lang/de.json:
--------------------------------------------------------------------------------
1 | {
2 | "30 Days": "30 Tage",
3 | ":amount Total": ":amount Gesamt",
4 | "Actions": "Aktionen"
5 | }
--------------------------------------------------------------------------------
/__tests__/__mocks__/lang/en.json:
--------------------------------------------------------------------------------
1 | {
2 | "30 Days": "30 Days",
3 | ":amount Total": ":Amount Total",
4 | "Accept Invitation": "Accept Invitation"
5 | }
6 |
--------------------------------------------------------------------------------
/__tests__/__mocks__/lang/en/auth.php:
--------------------------------------------------------------------------------
1 | 'These credentials do not match our records.',
17 | 'password' => 'The provided password is incorrect.',
18 | 'throttle' => 'Too many login attempts. Please try again in :seconds seconds.',
19 |
20 | ];
21 |
--------------------------------------------------------------------------------
/__tests__/__mocks__/lang/en/custom.php:
--------------------------------------------------------------------------------
1 | 'Welcome',
5 | 'apple_count' => ':count apple|:count apples',
6 | 'min_ago' => ':count minute ago|:count minutes ago',
7 | 'min_left_for_reason' => ':count minute left for :reason|:count minutes left for :reason',
8 | 'sub_level1' => [
9 | 'text' => 'I\'m sub level 1',
10 | 'sub_level2' => [
11 | 'text' => 'I\'m sub level 2',
12 | 'sub_level3' => [
13 | 'text' => 'I\'m sub level 3',
14 | 'sub_level4' => [
15 | 'text' => 'I\'m sub level 4'
16 | ]
17 | ]
18 | ]
19 | ],
20 | ];
21 |
--------------------------------------------------------------------------------
/__tests__/__mocks__/lang/en/random.php:
--------------------------------------------------------------------------------
1 | 'The :attribute field must be accepted.',
17 | 'between' => [
18 | 'array' => 'The :attribute field must have between :min and :max items.',
19 | ],
20 |
21 | /*
22 | |--------------------------------------------------------------------------
23 | | Custom Validation Language Lines
24 | |--------------------------------------------------------------------------
25 | |
26 | | Here you may specify custom validation messages for attributes using the
27 | | convention "attribute.rule" to name the lines. This makes it quick to
28 | | specify a specific custom language line for a given attribute rule.
29 | |
30 | */
31 |
32 | 'custom' => [
33 | 'attribute-name' => [
34 | 'rule-name' => 'custom-message',
35 | ],
36 | ],
37 |
38 | /*
39 | |--------------------------------------------------------------------------
40 | | Custom Validation Attributes
41 | |--------------------------------------------------------------------------
42 | |
43 | | The following language lines are used to swap our attribute placeholder
44 | | with something more reader friendly such as "E-Mail Address" instead
45 | | of "email". This simply helps us make our message more expressive.
46 | |
47 | */
48 |
49 | 'attributes' => [],
50 |
51 | ];
52 |
--------------------------------------------------------------------------------
/__tests__/__mocks__/lang/fr/auth.php:
--------------------------------------------------------------------------------
1 | 'Ces identifiants ne correspondent pas à nos enregistrements.',
16 | 'password' => 'Le mot de passe fourni est incorrect.',
17 | 'throttle' => 'Tentatives de connexion trop nombreuses. Veuillez essayer de nouveau dans :seconds secondes.',
18 | ];
19 |
--------------------------------------------------------------------------------
/__tests__/__mocks__/lang/fr/random.php:
--------------------------------------------------------------------------------
1 | 'Credenziali non valide.',
16 | 'password' => 'La password non è valida.',
17 | 'throttle' => 'Troppi tentativi di accesso. Riprova tra :seconds secondi.',
18 | ];
19 |
--------------------------------------------------------------------------------
/__tests__/__mocks__/lang/nl.json:
--------------------------------------------------------------------------------
1 | {
2 | "30 Days": "30 dagen",
3 | ":amount Total": ":amount Totaal",
4 | "Actions": "Acties"
5 | }
--------------------------------------------------------------------------------
/__tests__/__mocks__/lang/php_en.json:
--------------------------------------------------------------------------------
1 | {"auth.failed":"These credentials do not match our records.","auth.password":"The provided password is incorrect.","auth.throttle":"Too many login attempts. Please try again in :seconds seconds.","custom.Welcome":"Welcome","custom.apple_count":":count apple|:count apples","custom.min_ago":":count minute ago|:count minutes ago","custom.min_left_for_reason":":count minute left for :reason|:count minutes left for :reason","custom.sub_level1.text":"I'm sub level 1","custom.sub_level1.sub_level2.text":"I'm sub level 2","custom.sub_level1.sub_level2.sub_level3.text":"I'm sub level 3","custom.sub_level1.sub_level2.sub_level3.sub_level4.text":"I'm sub level 4","validation.accepted":"The :attribute field must be accepted.","validation.between.array":"The :attribute field must have between :min and :max items.","validation.custom.attribute-name.rule-name":"custom-message","validation.attributes":[]}
--------------------------------------------------------------------------------
/__tests__/__mocks__/lang/php_fr.json:
--------------------------------------------------------------------------------
1 | {"auth.failed":"Ces identifiants ne correspondent pas à nos enregistrements.","auth.password":"Le mot de passe fourni est incorrect.","auth.throttle":"Tentatives de connexion trop nombreuses. Veuillez essayer de nouveau dans :seconds secondes."}
--------------------------------------------------------------------------------
/__tests__/__mocks__/lang/php_it.json:
--------------------------------------------------------------------------------
1 | {"auth.failed":"Credenziali non valide.","auth.password":"La password non è valida.","auth.throttle":"Troppi tentativi di accesso. Riprova tra :seconds secondi."}
--------------------------------------------------------------------------------
/__tests__/__mocks__/lang/uk.json:
--------------------------------------------------------------------------------
1 | {
2 | "30 Days": "30 днів",
3 | ":amount Total": "Всього :amount",
4 | "Action": "Дія"
5 | }
--------------------------------------------------------------------------------
/__tests__/jest-setup.ts:
--------------------------------------------------------------------------------
1 | import '@testing-library/jest-dom';
2 | import fs from 'fs';
3 | import { resolve } from 'path';
4 | import locale from '../src/plugin/locale';
5 | import parser from '../src/plugin/parser';
6 |
7 | const clientFile: any = {};
8 | const serverFile: any = {};
9 |
10 | let files: { path: string; basename: string }[] = [];
11 |
12 | const mock = (dirName: string) => resolve(`./__tests__/__mocks__/${dirName}`);
13 |
14 | beforeAll(() => {
15 | const dirName = mock('lang');
16 | const phpLocales = locale.getPhpLocale(dirName);
17 |
18 | if (phpLocales.length > 0) {
19 | files = parser(dirName);
20 | }
21 |
22 | fs.readdirSync(dirName)
23 | .filter((basename) => !fs.statSync(dirName + '/' + basename).isDirectory() && basename.endsWith('.json'))
24 | .map((file) => {
25 | const path = dirName + '/' + file;
26 | clientFile[path] = import(path);
27 | serverFile[path] = require(path);
28 | });
29 | });
30 |
31 | export { clientFile, serverFile, mock };
32 |
--------------------------------------------------------------------------------
/__tests__/plugin/helper.test.ts:
--------------------------------------------------------------------------------
1 | import path from 'path';
2 | import { dirnameSanitize } from '../../src/plugin/helper';
3 |
4 | const { sep } = path;
5 |
6 | describe('helper', () => {
7 | it.each([
8 | ['/laravel/lang/', `${sep}laravel${sep}lang${sep}`],
9 | ['/laravel/lang', `${sep}laravel${sep}lang${sep}`],
10 | ['/laravel/lang//', `${sep}laravel${sep}lang${sep}`],
11 | ['/laravel/lang/sub', `${sep}laravel${sep}lang${sep}sub${sep}`],
12 |
13 | ['\\laravel\\lang\\', `${sep}laravel${sep}lang${sep}`],
14 | ['\\laravel\\lang', `${sep}laravel${sep}lang${sep}`],
15 | ['\\laravel\\lang\\\\', `${sep}laravel${sep}lang${sep}`],
16 | ['\\laravel\\lang\\sub', `${sep}laravel${sep}lang${sep}sub${sep}`]
17 | ])('dirnameSanitize', (rawDirname, expected) => {
18 | expect(dirnameSanitize(rawDirname)).toEqual(expected);
19 | });
20 | });
21 |
--------------------------------------------------------------------------------
/__tests__/plugin/locale.test.ts:
--------------------------------------------------------------------------------
1 | import { mock } from '../jest-setup';
2 |
3 | import locale from '../../src/plugin/locale';
4 |
5 | const jsonLocales = ['de', 'en', 'nl', 'uk'];
6 | const phpLocales = ['en', 'fr', 'it'];
7 |
8 | describe('locale', () => {
9 | it.each([
10 | [mock('lang'), jsonLocales],
11 | [mock('langError'), []]
12 | ])('getJsonLocale', (dirname, expected) => {
13 | expect(locale.getJsonLocale(dirname)).toMatchObject(expected);
14 | });
15 |
16 | it.each([
17 | [mock('lang'), phpLocales],
18 | [mock('langError'), []]
19 | ])('getJsonLocale', (dirname, expected) => {
20 | expect(locale.getPhpLocale(dirname)).toMatchObject(expected);
21 | });
22 | });
23 |
--------------------------------------------------------------------------------
/__tests__/plugin/parser.test.ts:
--------------------------------------------------------------------------------
1 | import { mock } from '../jest-setup';
2 | import parser from '../../src/plugin/parser';
3 |
4 | const result = [
5 | {
6 | basename: 'php_en.json',
7 | path: `${mock('lang')}/php_en.json`
8 | },
9 | {
10 | basename: 'php_fr.json',
11 | path: `${mock('lang')}/php_fr.json`
12 | },
13 | {
14 | basename: 'php_it.json',
15 | path: `${mock('lang')}/php_it.json`
16 | }
17 | ];
18 |
19 | describe('parser', () => {
20 | it.each([
21 | [mock('lang'), result],
22 | [mock('langError'), []]
23 | ])('parse php-file', (dirname, expected) => {
24 | expect(parser(dirname)).toMatchObject(expected);
25 | });
26 | });
27 |
--------------------------------------------------------------------------------
/__tests__/provider.test.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReplacementsInterface from '../src/interfaces/replacements';
3 |
4 | const locales = ['de', 'en', 'fr', 'it', 'nl', 'uk'];
5 |
6 | const tHookCases: [string, undefined | ReplacementsInterface, string | []][] = [
7 | ['130 Days', undefined, '130 Days'],
8 | [':amount Total', { amount: 'ten' }, 'Ten Total'],
9 | [':amount Total', { amount: 12 }, '12 Total'],
10 | ['Accept Invitation', undefined, 'Accept Invitation'],
11 |
12 | ['validation.accepted', { attribute: 'your' }, 'The your field must be accepted.'],
13 | ['validation.accepted', undefined, 'The :attribute field must be accepted.'],
14 | [
15 | 'validation.between.array',
16 | { attribute: '"my custom"', min: 1, max: 10 },
17 | 'The "my custom" field must have between 1 and 10 items.'
18 | ],
19 | ['validation.between.array', undefined, 'The :attribute field must have between :min and :max items.'],
20 | ['validation.custom.attribute-name.rule-name', undefined, 'custom-message'],
21 | ['validation.attributes', undefined, []],
22 |
23 | ['custom.Welcome', undefined, 'Welcome'],
24 | ['custom.sub_level1.text', undefined, "I'm sub level 1"],
25 | ['custom.sub_level1.sub_level2.text', undefined, "I'm sub level 2"],
26 | ['custom.sub_level1.sub_level2.sub_level3.text', undefined, "I'm sub level 3"],
27 | ['custom.sub_level1.sub_level2.sub_level3.sub_level4.text', undefined, "I'm sub level 4"]
28 | ];
29 |
30 | const tChoiceHookCases: [string, number, undefined | ReplacementsInterface, string][] = [
31 | ['custom.apple_count', 1, undefined, '1 apple'],
32 | ['custom.apple_count', 10, undefined, '10 apples'],
33 | ['custom.min_ago', 1, undefined, '1 minute ago'],
34 | ['custom.min_ago', 4, undefined, '4 minutes ago'],
35 | ['custom.min_left_for_reason', 1, undefined, '1 minute left for :reason'],
36 | ['custom.min_left_for_reason', 1, { reason: 'auto logout' }, '1 minute left for auto logout'],
37 | ['custom.min_left_for_reason', 15, { reason: 'auto logout' }, '15 minutes left for auto logout']
38 | ];
39 |
40 | test.todo('provider testing');
41 |
--------------------------------------------------------------------------------
/__tests__/utils/pluralization.test.ts:
--------------------------------------------------------------------------------
1 | import pluralization from '../../src/utils/pluralization';
2 |
3 | it.each([
4 | ['first', 'first', 1],
5 | ['first', 'first', 10],
6 | ['first', 'first|second', 1],
7 | ['second', 'first|second', 10],
8 | ['second', 'first|second', 0],
9 |
10 | ['first', '{0} first|{1}second', 0],
11 | ['first', '{1}first|{2}second', 1],
12 | ['second', '{1}first|{2}second', 2],
13 | ['first', '{2}first|{1}second', 2],
14 | ['second', '{9}first|{10}second', 0],
15 | ['first', '{9}first|{10}second', 1],
16 | ['', '{0}|{1}second', 0],
17 | ['', '{0}first|{1}', 1],
18 | ['first', '{1.3}first|{2.3}second', 1.3],
19 | ['second', '{1.3}first|{2.3}second', 2.3],
20 | ['first line', '{1}first line|{2}second', 1],
21 | ['first \nline', '{1}first \nline|{2}second', 1],
22 |
23 | ['first', '{0} first|[1,9]second', 0],
24 | ['second', '{0}first|[1,9]second', 1],
25 | ['second', '{0}first|[1,9]second', 10],
26 | ['first', '{0}first|[2,9]second', 1],
27 | ['second', '[4,*]first|[1,3]second', 1],
28 | ['first', '[4,*]first|[1,3]second', 100],
29 | ['second', '[1,5]first|[6,10]second', 7],
30 | ['first', '[*,4]first|[5,*]second', 1],
31 | ['second', '[5,*]first|[*,4]second', 1],
32 | ['second', '[5,*]first|[*,4]second', 0],
33 |
34 | ['first', '{0}first|[1,3]second|[4,*]third', 0],
35 | ['second', '{0}first|[1,3]second|[4,*]third', 1],
36 | ['second', '{0}first|[1,3]second|[4,*]third', 2],
37 | ['second', '{0}first|[1,3]second|[4,*]third', 3],
38 | ['third', '{0}first|[1,3]second|[4,*]third', 4],
39 | ['third', '{0}first|[1,3]second|[4,*]third', 100],
40 |
41 | ['second', 'first|second|third', 0],
42 | ['first', 'first|second|third', 1],
43 | ['second', 'first|second|third', 9],
44 |
45 | ['first', '{0} first | { 1 } second', 0],
46 | ['first', '[4,*]first | [1,3]second', 100]
47 | ])('translates pluralization', (expected, message, number) => {
48 | expect(pluralization(message, number, 'en')).toBe(expected);
49 | });
50 |
51 | it.each([
52 | ['en-US', 0, 'second'],
53 | ['en_US', 1, 'first'],
54 | ['en', 2, 'second'],
55 | ['fr', 0, 'first'],
56 | ['fr', 1, 'first'],
57 | ['fr', 2, 'second'],
58 | ['be', 0, 'third'],
59 | ['be', 1, 'first'],
60 | ['be', 3, 'second'],
61 | ['sk', 0, 'third'],
62 | ['sk', 1, 'first'],
63 | ['sk', 2, 'second'],
64 | ['ga', 0, 'third'],
65 | ['ga', 1, 'first'],
66 | ['ga', 2, 'second'],
67 | ['lt', 0, 'third'],
68 | ['lt', 1, 'first'],
69 | ['lt', 2, 'second'],
70 | ['sl', 0, 'fourth'],
71 | ['sl', 1, 'first'],
72 | ['sl', 2, 'second'],
73 | ['sl', 4, 'third'],
74 | ['mk', 0, 'second'],
75 | ['mk', 1, 'first'],
76 | ['mt', 0, 'second'],
77 | ['mt', 1, 'first'],
78 | ['mt', 11, 'third'],
79 | ['mt', 21, 'fourth'],
80 | ['lv', 0, 'first'],
81 | ['lv', 1, 'second'],
82 | ['lv', 2, 'third'],
83 | ['pl', 0, 'third'],
84 | ['pl', 1, 'first'],
85 | ['pl', 2, 'second'],
86 | ['cy', 0, 'fourth'],
87 | ['cy', 1, 'first'],
88 | ['cy', 2, 'second'],
89 | ['cy', 8, 'third'],
90 | ['ro', 0, 'second'],
91 | ['ro', 1, 'first'],
92 | ['ro', 21, 'third'],
93 | ['ar', 0, 'first'],
94 | ['ar', 1, 'second'],
95 | ['ar', 2, 'third'],
96 | ['ar', 3, 'fourth'],
97 | ['ar', 12, 'fifth'],
98 | ['ar', 99.1, 'sixth'],
99 | ['az', 0, 'first'],
100 | ['az', 1, 'first'],
101 | ['random', 0, 'first']
102 | ])('translates each locale with the correct plural', (locale, number, correctMessage) => {
103 | const message = 'first|second|third|fourth|fifth|sixth';
104 |
105 | expect(pluralization(message, number, locale)).toBe(correctMessage);
106 | });
107 |
--------------------------------------------------------------------------------
/__tests__/utils/recognizer.test.ts:
--------------------------------------------------------------------------------
1 | describe('recognizer', () => {
2 | test.todo('---');
3 | });
4 |
--------------------------------------------------------------------------------
/__tests__/utils/replacer.test.ts:
--------------------------------------------------------------------------------
1 | import replacer from '../../src/utils/replacer';
2 |
3 | it.each([
4 | ['some text', ':replace', 'some text'],
5 | ['some text', ':replace', 'some text'],
6 | [' some text ', ' :replace ', 'some text'],
7 | [' some text ', ':replace', ' some text '],
8 | ['\nsome text', '\n:replace', 'some text'],
9 | ['some\ntext', ':replace', 'some\ntext'],
10 |
11 | ['"some text"', '":replace"', 'some text'],
12 | ['some text
', ':replace
', 'some text'],
13 |
14 | ['some text', ':replace', 'some text'],
15 | ['Some text', ':Replace', 'some text'],
16 | ['SOME TEXT', ':REPLACE', 'some text'],
17 |
18 | ['Lorem Ipsum some text', 'Lorem Ipsum :replace', 'some text'],
19 | ['some text Lorem Ipsum', ':replace Lorem Ipsum', 'some text'],
20 | ['Lorem Ipsum some text Lorem Ipsum', 'Lorem Ipsum :replace Lorem Ipsum', 'some text'],
21 |
22 | ['some text some text', ':replace :replace', 'some text'],
23 | [':anyReplace', ':anyReplace', 'some text']
24 | ])('replacer', (expected, message, replace) => {
25 | expect(replacer(message, { replace })).toBe(expected);
26 | });
27 |
28 | test('Sentence replace', () => {
29 | const expected =
30 | 'It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged.';
31 |
32 | const message =
33 | 'It has survived not only :count centuries, but also the leap into :epochType typesetting, remaining :remaining unchanged.';
34 |
35 | const replacements = { count: 'five', epochType: 'electronic', remaining: 'essentially' };
36 |
37 | expect(replacer(message, replacements)).toBe(expected);
38 | });
39 |
--------------------------------------------------------------------------------
/__tests__/utils/resolver.test.ts:
--------------------------------------------------------------------------------
1 | import { clientFile, serverFile } from '../jest-setup';
2 | import resolver from '../../src/utils/resolver';
3 |
4 | const results = {
5 | en: {
6 | json: {
7 | '30 Days': '30 Days',
8 | ':amount Total': ':Amount Total',
9 | 'Accept Invitation': 'Accept Invitation'
10 | },
11 | php: {
12 | 'auth.failed': 'These credentials do not match our records.',
13 | 'auth.password': 'The provided password is incorrect.',
14 | 'auth.throttle': 'Too many login attempts. Please try again in :seconds seconds.',
15 | 'custom.Welcome': 'Welcome',
16 | 'custom.apple_count': ':count apple|:count apples',
17 | 'custom.min_ago': ':count minute ago|:count minutes ago',
18 | 'custom.sub_level1.text': "I'm sub level 1",
19 | 'custom.sub_level1.sub_level2.text': "I'm sub level 2",
20 | 'custom.sub_level1.sub_level2.sub_level3.text': "I'm sub level 3",
21 | 'custom.sub_level1.sub_level2.sub_level3.sub_level4.text': "I'm sub level 4",
22 | 'validation.accepted': 'The :attribute field must be accepted.',
23 | 'validation.between.array': 'The :attribute field must have between :min and :max items.',
24 | 'validation.custom.attribute-name.rule-name': 'custom-message',
25 | 'validation.attributes': []
26 | }
27 | },
28 | uk: {
29 | json: {
30 | '30 Days': '30 днів',
31 | ':amount Total': 'Всього :amount',
32 | Action: 'Дія'
33 | },
34 | php: {}
35 | },
36 | it: {
37 | json: {},
38 | php: {
39 | 'auth.failed': 'Credenziali non valide.',
40 | 'auth.password': 'La password non è valida.',
41 | 'auth.throttle': 'Troppi tentativi di accesso. Riprova tra :seconds secondi.'
42 | }
43 | }
44 | };
45 |
46 | describe('resolver', () => {
47 | // let clientFile: any;
48 | // let serverFile: any;
49 | //
50 | // beforeAll(() => {
51 | // clientFile = import.meta.glob('../__mocks__/lang/*.json');
52 | // serverFile = import.meta.globEager('../__mocks__/lang/*.json');
53 | // });
54 |
55 | it.each([
56 | ['en', results.en.json, results.en.php],
57 | ['uk', results.uk.json, results.uk.php],
58 | ['it', results.it.json, results.it.php],
59 | ['ro', {}, {}],
60 | ['error', {}, {}]
61 | ])('client', async (locale, resultJson, resultPhp) => {
62 | const promises = resolver(clientFile, locale);
63 | const [responseJson, responsePhp] = await Promise.all(promises);
64 |
65 | expect(responseJson.default).toMatchObject(resultJson);
66 | expect(responsePhp.default).toMatchObject(resultPhp);
67 | });
68 |
69 | it.each([
70 | ['en', results.en.json, results.en.php],
71 | ['uk', results.uk.json, results.uk.php],
72 | ['it', results.it.json, results.it.php],
73 | ['ro', {}, {}],
74 | ['error', {}, {}]
75 | ])('client', async (locale, resultJson, resultPhp) => {
76 | const responses = resolver(serverFile, locale);
77 | const [responseJson, responsePhp] = responses;
78 |
79 | expect(responseJson.default).toMatchObject(resultJson);
80 | expect(responsePhp.default).toMatchObject(resultPhp);
81 | });
82 | });
83 |
--------------------------------------------------------------------------------
/babel.config.json:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["@babel/preset-env", "@babel/preset-react", "@babel/preset-typescript"]
3 | }
--------------------------------------------------------------------------------
/jest.config.json:
--------------------------------------------------------------------------------
1 | {
2 | "testEnvironment": "jsdom",
3 | "setupFilesAfterEnv": ["/__tests__/jest-setup.ts"],
4 | "testPathIgnorePatterns": ["jest-setup.ts", "__mocks__"]
5 | }
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "laravel-react-i18n",
3 | "version": "2.0.5",
4 | "author": {
5 | "name": "Eugene Meles",
6 | "email": "xmelesx@gmail.com"
7 | },
8 | "keywords": [
9 | "vite-plugin",
10 | "vite-plugin-laravel",
11 | "laravel",
12 | "react",
13 | "i18n",
14 | "inertiajs"
15 | ],
16 | "engines": {
17 | "npm": ">=9.0.0",
18 | "node": ">=16.0.0"
19 | },
20 | "repository": "https://github.com/EugeneMeles/laravel-react-i18n",
21 | "license": "MIT",
22 | "description": "Allows to connect your `Laravel` Framework localization files with `React`.",
23 | "main": "dist/cjs/index.js",
24 | "module": "dist/index.js",
25 | "types": "dist/index.d.ts",
26 | "scripts": {
27 | "test": "jest",
28 | "prettier": "prettier -c \"src/**/*.(ts|tsx)\" --write",
29 | "build": "npm run build:client && npm run build:commonjs && npm run build:plugin",
30 | "build:client": "tsc -p tsconfig.client.json",
31 | "build:commonjs": "tsc -p tsconfig.commonjs.json",
32 | "build:plugin": "tsc -p tsconfig.plugin.json",
33 | "prepare": "npm run build",
34 | "postpublish": "PACKAGE_VERSION=$(cat package.json | grep \\\"version\\\" | head -1 | awk -F: '{ print $2 }' | sed 's/[\",]//g' | tr -d '[[:space:]]') && git tag v$PACKAGE_VERSION && git push --tags"
35 | },
36 | "devDependencies": {
37 | "@babel/core": "^7.21.8",
38 | "@babel/preset-env": "^7.21.5",
39 | "@babel/preset-react": "^7.18.6",
40 | "@babel/preset-typescript": "^7.21.5",
41 | "@testing-library/jest-dom": "^5.16.5",
42 | "@testing-library/react": "^14.0.0",
43 | "@types/jest": "^29.5.1",
44 | "@types/node": "^18.16.8",
45 | "@types/react": "^18.2.6",
46 | "@types/react-dom": "^18.2.4",
47 | "@typescript-eslint/eslint-plugin": "^5.59.5",
48 | "@typescript-eslint/parser": "^5.59.5",
49 | "eslint": "^8.40.0",
50 | "eslint-config-airbnb-typescript": "^17.0.0",
51 | "eslint-config-prettier": "^8.8.0",
52 | "eslint-plugin-import": "^2.27.5",
53 | "eslint-plugin-jest": "^27.2.1",
54 | "eslint-plugin-prettier": "^4.2.1",
55 | "eslint-plugin-react": "^7.32.2",
56 | "jest": "^29.5.0",
57 | "jest-environment-jsdom": "^29.5.0",
58 | "prettier": "^2.8.8",
59 | "typescript": "^5.0.4",
60 | "vite": "^4.3.5"
61 | },
62 | "dependencies": {
63 | "php-array-reader": "^2.1.2",
64 | "react": "^18.2.0"
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/src/@types/php-array-reader.d.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Declaration patch for module `php-array-reader`
3 | *
4 | * (fromString)
5 | */
6 | declare module 'php-array-reader' {
7 | export const fromString: (phpString: string) => object;
8 | }
9 |
--------------------------------------------------------------------------------
/src/context.ts:
--------------------------------------------------------------------------------
1 | import { createContext } from 'react';
2 |
3 | import ContextInterface from './interfaces/context';
4 |
5 | export const Context = createContext({
6 | t: (key) => '',
7 | tChoice: (key) => '',
8 | currentLocale: () => '',
9 | getLocales: () => [''],
10 | isLocale: (locale) => true,
11 | loading: true,
12 | setLocale: (locale) => {}
13 | });
14 |
--------------------------------------------------------------------------------
/src/contrib/get-plural-index.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable */
2 |
3 | /**
4 | * Get the index to use for pluralization.
5 | * The plural rules are derived from code of the Zend Framework.
6 | *
7 | * @category Zend
8 | * @package Zend_Locale
9 | * @public https://github.com/zendframework/zf1/blob/master/library/Zend/Translate/Plural.php
10 | * @copyright 2005-2015 Zend Technologies USA Inc. http://www.zend.com
11 | * @license http://framework.zend.com/license New BSD License
12 | *
13 | * @param {String} locale
14 | * @param {Number} number
15 | * @return {Number}
16 | */
17 | export function getPluralIndex(locale: string, number: number) {
18 | locale = locale.replace('-', '_');
19 |
20 | if (locale === 'pt_BR') {
21 | // temporary set a locale for brazilian
22 | locale = 'xbr';
23 | }
24 |
25 | if (locale.length > 3) {
26 | locale = locale.substring(0, locale.lastIndexOf('_'));
27 | }
28 |
29 | switch (locale) {
30 | case 'az':
31 | case 'bo':
32 | case 'dz':
33 | case 'id':
34 | case 'ja':
35 | case 'jv':
36 | case 'ka':
37 | case 'km':
38 | case 'kn':
39 | case 'ko':
40 | case 'ms':
41 | case 'th':
42 | case 'tr':
43 | case 'vi':
44 | case 'zh':
45 | return 0;
46 | case 'af':
47 | case 'bn':
48 | case 'bg':
49 | case 'ca':
50 | case 'da':
51 | case 'de':
52 | case 'el':
53 | case 'en':
54 | case 'eo':
55 | case 'es':
56 | case 'et':
57 | case 'eu':
58 | case 'fa':
59 | case 'fi':
60 | case 'fo':
61 | case 'fur':
62 | case 'fy':
63 | case 'gl':
64 | case 'gu':
65 | case 'ha':
66 | case 'he':
67 | case 'hu':
68 | case 'is':
69 | case 'it':
70 | case 'ku':
71 | case 'lb':
72 | case 'ml':
73 | case 'mn':
74 | case 'mr':
75 | case 'nah':
76 | case 'nb':
77 | case 'ne':
78 | case 'nl':
79 | case 'nn':
80 | case 'no':
81 | case 'om':
82 | case 'or':
83 | case 'pa':
84 | case 'pap':
85 | case 'ps':
86 | case 'pt':
87 | case 'so':
88 | case 'sq':
89 | case 'sv':
90 | case 'sw':
91 | case 'ta':
92 | case 'te':
93 | case 'tk':
94 | case 'ur':
95 | case 'zu':
96 | return number === 1 ? 0 : 1;
97 | case 'am':
98 | case 'bh':
99 | case 'fil':
100 | case 'fr':
101 | case 'gun':
102 | case 'hi':
103 | case 'ln':
104 | case 'mg':
105 | case 'nso':
106 | case 'xbr':
107 | case 'ti':
108 | case 'wa':
109 | return number === 0 || number === 1 ? 0 : 1;
110 | case 'be':
111 | case 'bs':
112 | case 'hr':
113 | case 'ru':
114 | case 'sr':
115 | case 'uk':
116 | return number % 10 === 1 && number % 100 !== 11
117 | ? 0
118 | : number % 10 >= 2 && number % 10 <= 4 && (number % 100 < 10 || number % 100 >= 20)
119 | ? 1
120 | : 2;
121 | case 'cs':
122 | case 'sk':
123 | return number === 1 ? 0 : number >= 2 && number <= 4 ? 1 : 2;
124 | case 'ga':
125 | return number === 1 ? 0 : number === 2 ? 1 : 2;
126 | case 'lt':
127 | return number % 10 === 1 && number % 100 !== 11
128 | ? 0
129 | : number % 10 >= 2 && (number % 100 < 10 || number % 100 >= 20)
130 | ? 1
131 | : 2;
132 | case 'sl':
133 | return number % 100 === 1 ? 0 : number % 100 === 2 ? 1 : number % 100 === 3 || number % 100 === 4 ? 2 : 3;
134 | case 'mk':
135 | return number % 10 === 1 ? 0 : 1;
136 | case 'mt':
137 | return number === 1
138 | ? 0
139 | : number === 0 || (number % 100 > 1 && number % 100 < 11)
140 | ? 1
141 | : number % 100 > 10 && number % 100 < 20
142 | ? 2
143 | : 3;
144 | case 'lv':
145 | return number === 0 ? 0 : number % 10 === 1 && number % 100 !== 11 ? 1 : 2;
146 | case 'pl':
147 | return number === 1
148 | ? 0
149 | : number % 10 >= 2 && number % 10 <= 4 && (number % 100 < 12 || number % 100 > 14)
150 | ? 1
151 | : 2;
152 | case 'cy':
153 | return number === 1 ? 0 : number === 2 ? 1 : number === 8 || number === 11 ? 2 : 3;
154 | case 'ro':
155 | return number === 1 ? 0 : number === 0 || (number % 100 > 0 && number % 100 < 20) ? 1 : 2;
156 | case 'ar':
157 | return number === 0
158 | ? 0
159 | : number === 1
160 | ? 1
161 | : number === 2
162 | ? 2
163 | : number >= 3 && number <= 10
164 | ? 3
165 | : number >= 11 && number <= 99
166 | ? 4
167 | : 5;
168 | default:
169 | return 0;
170 | }
171 | }
172 |
--------------------------------------------------------------------------------
/src/hook.ts:
--------------------------------------------------------------------------------
1 | import { useContext } from 'react';
2 |
3 | import { Context } from './context';
4 |
5 | import ContextInterface from './interfaces/context';
6 |
7 | export default function useLaravelReactI18n() {
8 | return useContext>(Context);
9 | }
10 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | import LaravelReactI18nProvider from './provider';
2 | import useLaravelReactI18n from './hook';
3 |
4 | export { LaravelReactI18nProvider, useLaravelReactI18n };
5 |
--------------------------------------------------------------------------------
/src/interfaces/context.ts:
--------------------------------------------------------------------------------
1 | import ReplacementsInterface from './replacements';
2 |
3 | /**
4 | *
5 | */
6 | export default interface ContextInterface {
7 | currentLocale: () => string;
8 | getLocales: () => string[];
9 | isLocale: (locale: string) => boolean;
10 | loading: boolean;
11 | setLocale: (locale: string) => void;
12 | t: (key: T, replacements?: ReplacementsInterface) => string;
13 | tChoice: (key: T, number: number, replacements?: ReplacementsInterface) => string;
14 | }
15 |
--------------------------------------------------------------------------------
/src/interfaces/default-options.ts:
--------------------------------------------------------------------------------
1 | import LocaleFileInterface from './locale-file';
2 |
3 | /**
4 | * The Interface that is responsible for the default options.
5 | */
6 | export default interface DefaultOptionsInterface {
7 | fallbackLocale: string;
8 | locale: string;
9 | prevLocale: string;
10 | files: Record | Record Promise>;
11 | }
12 |
--------------------------------------------------------------------------------
/src/interfaces/i18n-provider-props.ts:
--------------------------------------------------------------------------------
1 | import { ReactNode } from 'react';
2 |
3 | import OptionsInterface from './options';
4 |
5 | /**
6 | *
7 | */
8 | export default interface I18nProviderProps extends OptionsInterface {
9 | children: ReactNode;
10 | ssr?: boolean;
11 | }
12 |
--------------------------------------------------------------------------------
/src/interfaces/locale-file.ts:
--------------------------------------------------------------------------------
1 | /**
2 | *
3 | */
4 | export default interface LocaleFileInterface {
5 | default: {
6 | [key: string]: string;
7 | };
8 | }
9 |
--------------------------------------------------------------------------------
/src/interfaces/options-provider.ts:
--------------------------------------------------------------------------------
1 | import OptionsInterface from './options';
2 |
3 | /**
4 | * The Interface that is responsible for the OptionsProvider provided.
5 | */
6 | export default interface OptionsProviderInterface extends OptionsInterface {
7 | prevLocale?: string;
8 | }
9 |
--------------------------------------------------------------------------------
/src/interfaces/options.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * The Interface that is responsible for the Options provided.
3 | */
4 | export default interface OptionsInterface {
5 | fallbackLocale?: string;
6 | locale?: string;
7 | files: Record | Record Promise>;
8 | }
9 |
--------------------------------------------------------------------------------
/src/interfaces/replacements.ts:
--------------------------------------------------------------------------------
1 | /**
2 | *
3 | */
4 | export default interface ReplacementsInterface {
5 | [key: string]: string | number;
6 | }
7 |
--------------------------------------------------------------------------------
/src/plugin/helper.ts:
--------------------------------------------------------------------------------
1 | import path from 'path';
2 |
3 | /**
4 | *
5 | * @param rawDirname
6 | */
7 | export function dirnameSanitize(rawDirname: string): string {
8 | return rawDirname.replace(/[\\/]+/g, path.sep).replace(/[\\/]+$/, '') + path.sep;
9 | }
10 |
--------------------------------------------------------------------------------
/src/plugin/key-type.ts:
--------------------------------------------------------------------------------
1 | import fs from 'fs';
2 | import path from 'path';
3 |
4 | /**
5 | *
6 | * @param dirname
7 | * @param basename
8 | */
9 | export function convertToKeyType(dirname: string, basename: string): string {
10 | const result = fs.readFileSync(`${dirname + path.sep + basename}.json`, 'utf8');
11 | const obj = Object.entries(JSON.parse(result));
12 |
13 | let str = '';
14 | obj.forEach(([key], index) => {
15 | // Escaping a key
16 | const escKey = key.replace(/[!@#$%^&*()+=\-[\]\\';,/{}|":<>?~_]/g, '\\$&');
17 |
18 | str = obj.length === 1 || obj.length - 1 === index ? `${str}'${escKey}'` : `${str}'${escKey}'|`;
19 | });
20 |
21 | return str;
22 | }
23 |
24 | /**
25 | *
26 | * @param keys
27 | * @param dirname
28 | */
29 | export function saveKeyTypeToFile(keys: string, dirname = 'resources/js') {
30 | const sanitizeDirname = dirname.replace(/[\\/]$/, '') + path.sep;
31 | const data = `export type I18nKeyType = ${keys};`.replace(/[\r\n]+/g, '');
32 |
33 | fs.writeFileSync(`${sanitizeDirname}LaravelReactI18n.types.ts`, data);
34 | }
35 |
--------------------------------------------------------------------------------
/src/plugin/locale.ts:
--------------------------------------------------------------------------------
1 | import fs from 'fs';
2 | import path from 'path';
3 |
4 | import { dirnameSanitize } from './helper';
5 |
6 | export default {
7 | /**
8 | *
9 | * @param dirname
10 | */
11 | getJsonLocale: (dirname: string): string[] => {
12 | const sanitizedDirname = dirnameSanitize(dirname);
13 |
14 | if (!fs.existsSync(sanitizedDirname)) {
15 | return [];
16 | }
17 |
18 | return fs
19 | .readdirSync(sanitizedDirname)
20 | .filter((basename) => {
21 | const fullPath = path.join(sanitizedDirname, basename);
22 | return fs.statSync(fullPath).isFile() && !basename.startsWith('php_');
23 | })
24 | .map((basename) => basename.replace(/\.json$/, ''))
25 | .sort();
26 | },
27 |
28 | /**
29 | *
30 | * @param dirname
31 | */
32 | getPhpLocale: (dirname: string): string[] => {
33 | const sanitizedDirname = dirnameSanitize(dirname);
34 |
35 | if (!fs.existsSync(sanitizedDirname)) {
36 | return [];
37 | }
38 |
39 | return fs
40 | .readdirSync(sanitizedDirname)
41 | .filter((folder) => {
42 | const fullPath = path.join(sanitizedDirname, folder);
43 | return fs.statSync(fullPath).isDirectory();
44 | })
45 | .filter((folder) => {
46 | const phpFiles = fs.readdirSync(path.join(sanitizedDirname, folder));
47 | return phpFiles.some((basename) => /\.php$/.test(basename));
48 | })
49 | .sort();
50 | }
51 | };
52 |
--------------------------------------------------------------------------------
/src/plugin/parser.ts:
--------------------------------------------------------------------------------
1 | import fs from 'fs';
2 | import path from 'path';
3 | import { fromString } from 'php-array-reader';
4 |
5 | import { dirnameSanitize } from './helper';
6 |
7 | /**
8 | *
9 | * Parse array of PHP file.
10 | *
11 | * @param dirname
12 | */
13 | export default function parser(dirname: string): { path: string; basename: string }[] {
14 | const sanitizedDirname = dirnameSanitize(dirname);
15 |
16 | if (!fs.existsSync(sanitizedDirname)) {
17 | return [];
18 | }
19 |
20 | return fs
21 | .readdirSync(sanitizedDirname)
22 | .filter((locale) => fs.statSync(path.join(sanitizedDirname, locale)).isDirectory())
23 | .sort()
24 | .map((locale) => {
25 | const translations = convertToDottedKey(getObjectTranslation(path.join(sanitizedDirname, locale)));
26 | return { locale, trans: translations };
27 | })
28 | .filter(({ trans }) => Object.keys(trans).length > 0)
29 | .map(({ locale, trans }) => {
30 | const basename = `php_${locale}.json`;
31 | fs.writeFileSync(path.join(sanitizedDirname, basename), JSON.stringify(trans));
32 |
33 | return { basename, path: path.join(sanitizedDirname, basename) };
34 | });
35 | }
36 |
37 | /**
38 | *
39 | * @param source
40 | * @param target
41 | * @param keys
42 | */
43 | function convertToDottedKey(
44 | source: Record,
45 | target: Record = {},
46 | keys: string[] = []
47 | ): Record {
48 | Object.entries(source).forEach(([key, value]) => {
49 | const newPrefix = [...keys, key];
50 |
51 | if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
52 | convertToDottedKey(value as Record, target, newPrefix);
53 | } else {
54 | target[newPrefix.join('.')] = value as string;
55 | }
56 | });
57 |
58 | return target;
59 | }
60 |
61 | /**
62 | *
63 | * @param dirname
64 | */
65 | function getObjectTranslation(dirname: string): Record {
66 | const translations: Record = {};
67 |
68 | fs.readdirSync(dirname).forEach((basename) => {
69 | const absoluteFile = path.join(dirname, basename);
70 | const key = basename.replace(/\.\w+$/, '');
71 |
72 | if (fs.statSync(absoluteFile).isDirectory()) {
73 | translations[key] = getObjectTranslation(absoluteFile);
74 | } else {
75 | const fileContent = fs.readFileSync(absoluteFile, 'utf-8');
76 | translations[key] = fromString(fileContent);
77 | }
78 | });
79 |
80 | return translations;
81 | }
82 |
--------------------------------------------------------------------------------
/src/provider.ts:
--------------------------------------------------------------------------------
1 | import { createElement, useEffect, useState } from 'react';
2 |
3 | import { Context } from './context';
4 | import type DefaultOptionsInterface from './interfaces/default-options';
5 | import type I18nProviderProps from './interfaces/i18n-provider-props';
6 | import type ReplacementsInterface from './interfaces/replacements';
7 | import pluralization from './utils/pluralization';
8 | import recognizer from './utils/recognizer';
9 | import replacer from './utils/replacer';
10 | import resolver from './utils/resolver';
11 |
12 | /**
13 | *
14 | */
15 | const isServer = typeof window === 'undefined';
16 |
17 | /**
18 | * Map object for translations.
19 | */
20 | const translation = new Map();
21 |
22 | /**
23 | * Get document lang meta from HTML.
24 | */
25 | const documentLang =
26 | typeof document !== 'undefined' ? document?.documentElement?.lang?.replace('-', '_') || 'en' : 'en';
27 |
28 | /**
29 | * The default options.
30 | */
31 | const defaultOptions: DefaultOptionsInterface = {
32 | locale: documentLang,
33 | fallbackLocale: documentLang,
34 | prevLocale: documentLang,
35 | files: {}
36 | };
37 |
38 | /**
39 | * Laravel React I18n Provider:
40 | */
41 | export default function LaravelReactI18nProvider({ children, ssr, ...currentOptions }: I18nProviderProps) {
42 | const [isFirstRender, setIsFirstRender] = useState(true);
43 | const [loading, setLoading] = useState(!isServer);
44 | const [options, setOptions] = useState({
45 | ...defaultOptions,
46 | ...currentOptions
47 | });
48 | const { getLocales, isLocale } = recognizer(options.files);
49 |
50 | // Determine if files are eagerly loaded.
51 | const filesAreEagerlyLoaded = Object.values(options.files).every(
52 | (value) => typeof value === 'object' && value !== null
53 | );
54 |
55 | const { locale, fallbackLocale } = options;
56 |
57 | if (!translation.get(locale)) {
58 | if (filesAreEagerlyLoaded) {
59 | fetchLocaleSync(locale);
60 | } else if (isServer) {
61 | fetchLocaleServer(locale);
62 | }
63 | }
64 |
65 | if (locale !== fallbackLocale && !translation.get(fallbackLocale)) {
66 | if (filesAreEagerlyLoaded) {
67 | fetchLocaleSync(fallbackLocale);
68 | } else if (isServer) {
69 | fetchLocaleServer(fallbackLocale);
70 | }
71 | }
72 |
73 | useEffect(() => {
74 | if (!filesAreEagerlyLoaded) {
75 | if (!translation.get(locale)) fetchLocaleClient(locale);
76 | if (locale !== fallbackLocale && !translation.get(fallbackLocale)) fetchLocaleClient(fallbackLocale);
77 | }
78 | }, [options.locale]);
79 |
80 | function fetchLocaleSync(locale: string): void {
81 | const responses = resolver(options.files, locale);
82 |
83 | for (const response of responses) {
84 | translation.set(locale, {
85 | ...(translation.get(locale) || {}),
86 | ...response.default
87 | });
88 | }
89 | }
90 |
91 | /**
92 | * Initialise translations for server.
93 | */
94 | if (isServer) {
95 | const { locale, fallbackLocale } = options;
96 |
97 | if (!translation.get(locale)) fetchLocaleServer(locale);
98 | if (locale !== fallbackLocale && !translation.get(fallbackLocale)) fetchLocaleServer(fallbackLocale);
99 | }
100 |
101 | /**
102 | * Fetching locale for client side.
103 | */
104 | function fetchLocaleClient(locale: string): void {
105 | const promises = resolver(options.files, locale);
106 |
107 | setLoading(true);
108 | Promise.all(promises)
109 | .then((responses) => {
110 | for (const response of responses) {
111 | translation.set(locale, {
112 | ...(translation.get(locale) || {}),
113 | ...response.default
114 | });
115 | }
116 | })
117 | .then(() => {
118 | if (isFirstRender) setIsFirstRender(false);
119 | setLoading(false);
120 | });
121 | }
122 |
123 | /**
124 | * Fetching locale for server side.
125 | */
126 | function fetchLocaleServer(locale: string): void {
127 | const responses = resolver(options.files, locale);
128 |
129 | for (const response of responses) {
130 | translation.set(locale, {
131 | ...(translation.get(locale) || {}),
132 | ...response.default
133 | });
134 | }
135 | }
136 |
137 | /**
138 | * Get the translation for the given key.
139 | */
140 | function t(key: string, replacements: ReplacementsInterface = {}): string {
141 | const { locale, fallbackLocale, prevLocale } = options;
142 |
143 | let message = translation.get(fallbackLocale)?.[key] ? translation.get(fallbackLocale)[key] : key;
144 |
145 | if (isLocale(locale)) {
146 | if (translation.get(locale)?.[key]) {
147 | message = translation.get(locale)[key];
148 | } else if (translation.get(prevLocale)?.[key]) {
149 | message = translation.get(prevLocale)[key];
150 | } else if (translation.get(fallbackLocale)?.[key]) {
151 | message = translation.get(fallbackLocale)[key];
152 | }
153 | }
154 |
155 | return replacer(message, replacements);
156 | }
157 |
158 | /**
159 | * Translates the given message based on a count.
160 | */
161 | function tChoice(key: string, number: number, replacements: ReplacementsInterface = {}): string {
162 | const message = t(key, replacements);
163 | const locale = isLocale(options.locale) ? options.locale : options.fallbackLocale;
164 |
165 | return replacer(pluralization(message, number, locale), {
166 | ...replacements,
167 | count: number.toString()
168 | });
169 | }
170 |
171 | /**
172 | * Set locale.
173 | */
174 | function setLocale(locale: string) {
175 | if (!isServer) {
176 | // When setting the HTML lang attribute, hyphen must be use instead of underscore.
177 | document.documentElement.setAttribute('lang', locale.replace('_', '-'));
178 | }
179 |
180 | setOptions((prevState) => ({
181 | ...options,
182 | locale,
183 | prevLocale: prevState.locale
184 | }));
185 | }
186 |
187 | /**
188 | * Current locale.
189 | */
190 | function currentLocale(): string {
191 | return options.locale || options.fallbackLocale;
192 | }
193 |
194 | return createElement(
195 | Context.Provider,
196 | {
197 | value: {
198 | t,
199 | tChoice,
200 | loading,
201 | isLocale,
202 | getLocales,
203 | currentLocale,
204 | setLocale
205 | }
206 | },
207 | children
208 | );
209 | }
210 |
--------------------------------------------------------------------------------
/src/utils/pluralization.ts:
--------------------------------------------------------------------------------
1 | import { getPluralIndex } from '../contrib/get-plural-index';
2 |
3 | /**
4 | * Select a proper translation string based on the given number.
5 | *
6 | * @param message
7 | * @param number
8 | * @param locale
9 | */
10 | export default function pluralization(message: string, number: number, locale: string): string {
11 | let segments = message.split('|');
12 | const extracted = extract(segments, number);
13 |
14 | if (extracted !== null) {
15 | return extracted.trim();
16 | }
17 |
18 | segments = stripConditions(segments);
19 | const pluralIndex = getPluralIndex(locale, number);
20 |
21 | return segments.length === 1 || !segments[pluralIndex] ? segments[0] : segments[pluralIndex];
22 | }
23 |
24 | /**
25 | * Extract a translation string using inline conditions.
26 | *
27 | * @param segments
28 | * @param number
29 | */
30 | function extract(segments: string[], number: number): string | null {
31 | let result: string | null = null;
32 |
33 | segments.forEach((segment) => {
34 | if (result !== null) return;
35 | result = extractFromString(segment, number);
36 | });
37 |
38 | return result;
39 | }
40 |
41 | /**
42 | * Get the translation string if the condition matches.
43 | *
44 | * @param part
45 | * @param number
46 | */
47 | function extractFromString(part: string, number: number): string | null {
48 | const matches = part.match(/^[{[]([^,{}\[\]]*),?([^{}\[\]]*)[}\]]([\s\S]*)/);
49 |
50 | if (!matches) return null;
51 |
52 | const [, from, to, value] = matches;
53 |
54 | if ((from === '*' || number >= parseFloat(from)) && (to === '*' || number <= parseFloat(to))) {
55 | return value;
56 | }
57 |
58 | return from && parseFloat(from) === number ? value : null;
59 | }
60 |
61 | /**
62 | * Strip the inline conditions from each segment, just leaving the text.
63 | *
64 | * @param segments
65 | */
66 | function stripConditions(segments: string[]): string[] {
67 | return segments.map((part) => part.replace(/^[{[]([^[\]{}]*)[}\]]/, ''));
68 | }
69 |
--------------------------------------------------------------------------------
/src/utils/recognizer.ts:
--------------------------------------------------------------------------------
1 | /**
2 | *
3 | * @param files
4 | */
5 | export default function recognizer(files: Record | Record Promise>) {
6 | const jsonLocales = new Set();
7 | const phpLocales = new Set();
8 | const jsonFileLocales: Record = {};
9 | const phpFileLocales: Record = {};
10 |
11 | Object.keys(files).forEach((file) => {
12 | const match = file.match(/.*\/(php_)?(.*)\.json$/);
13 | if (match) {
14 | const [, isPhp, locale] = match;
15 |
16 | if (isPhp) {
17 | phpLocales.add(locale);
18 | phpFileLocales[locale] = file;
19 | } else {
20 | jsonLocales.add(locale);
21 | jsonFileLocales[locale] = file;
22 | }
23 | }
24 | });
25 |
26 | const locales = Array.from(new Set([...jsonLocales, ...phpLocales])).sort();
27 |
28 | return {
29 | isLocale: (locale: string): boolean => locales.includes(locale),
30 | getLocales: () => locales,
31 | isJsonLocale: (locale: string): boolean => jsonLocales.has(locale),
32 | getJsonLocales: () => Array.from(jsonLocales).sort(),
33 | isPhpLocale: (locale: string): boolean => phpLocales.has(locale),
34 | getPhpLocales: () => Array.from(phpLocales).sort(),
35 | getJsonFile: (locale: string): string => jsonFileLocales[locale],
36 | getPhpFile: (locale: string): string => phpFileLocales[locale]
37 | };
38 | }
39 |
--------------------------------------------------------------------------------
/src/utils/replacer.ts:
--------------------------------------------------------------------------------
1 | import ReplacementsInterface from '../interfaces/replacements';
2 |
3 | /**
4 | * Make the place-holder replacements on a line.
5 | *
6 | * @param message
7 | * @param replacements
8 | */
9 | export default function replacer(message: string, replacements?: ReplacementsInterface): string {
10 | if (!replacements) return message;
11 |
12 | const patterns = Object.entries(replacements).flatMap(([key, value]) => [
13 | { pattern: new RegExp(`:${key}`, 'g'), replacement: value.toString() },
14 | { pattern: new RegExp(`:${key.toUpperCase()}`, 'g'), replacement: value.toString().toUpperCase() },
15 | { pattern: new RegExp(`:${capitalize(key)}`, 'g'), replacement: capitalize(value.toString()) }
16 | ]);
17 |
18 | return patterns.reduce((result, { pattern, replacement }) => result.replace(pattern, replacement), message);
19 | }
20 |
21 | /**
22 | * Capitalizing string.
23 | *
24 | * @param str
25 | */
26 | function capitalize(str: string): string {
27 | return str ? str[0].toUpperCase() + str.slice(1) : '';
28 | }
29 |
--------------------------------------------------------------------------------
/src/utils/resolver.ts:
--------------------------------------------------------------------------------
1 | import recognizer from '../utils/recognizer';
2 |
3 | import LocaleFileInterface from '../interfaces/locale-file';
4 |
5 | /**
6 | * Resolver the language file.
7 | *
8 | * @param files
9 | * @param locale
10 | */
11 | export default function resolver(
12 | files: Record | Record Promise>,
13 | locale: string
14 | ): LocaleFileInterface[] {
15 | const { isJsonLocale, isPhpLocale, getJsonFile, getPhpFile } = recognizer(files);
16 |
17 | const jsonLocale = isJsonLocale(locale) ? files[getJsonFile(locale)] : undefined;
18 | const phpLocale = isPhpLocale(locale) ? files[getPhpFile(locale)] : undefined;
19 |
20 | const getType = (obj: string) => Object.prototype.toString.call(obj);
21 |
22 | if (
23 | ['[object Promise]', '[object Module]'].includes(getType(jsonLocale)) ||
24 | ['[object Promise]', '[object Module]'].includes(getType(phpLocale))
25 | ) {
26 | return [jsonLocale ? jsonLocale : { default: {} }, phpLocale ? phpLocale : { default: {} }];
27 | }
28 |
29 | if (getType(jsonLocale) === '[object Object]' || getType(phpLocale) === '[object Object]') {
30 | return [{ default: jsonLocale || {} }, { default: phpLocale || {} }];
31 | }
32 |
33 | if (getType(jsonLocale) === '[object Function]' || getType(phpLocale) === '[object Function]') {
34 | return [jsonLocale ? jsonLocale() : { default: {} }, phpLocale ? phpLocale() : { default: {} }];
35 | }
36 |
37 | return [{ default: {} }, { default: {} }];
38 | }
39 |
--------------------------------------------------------------------------------
/src/vite.ts:
--------------------------------------------------------------------------------
1 | import fs from 'fs';
2 |
3 | import { createLogger } from 'vite';
4 |
5 | import { convertToKeyType, saveKeyTypeToFile } from './plugin/key-type';
6 | import parser from './plugin/parser';
7 | import locale from './plugin/locale';
8 |
9 | interface ConfigInterface {
10 | langDirname?: string;
11 | typeDestinationPath?: string;
12 | typeTranslationKeys?: boolean;
13 | }
14 |
15 | /**
16 | *
17 | */
18 | export default function i18n(config?: ConfigInterface) {
19 | const langDirname = config?.langDirname ? config.langDirname : 'lang';
20 |
21 | const logger = createLogger('info', { prefix: '[laravel-react-i18n]' });
22 |
23 | let isPhpLocale = false;
24 | let files: { path: string; basename: string }[] = [];
25 | let exitHandlersBound = false;
26 | let jsonLocales: string[] = [];
27 | let phpLocales: string[] = [];
28 |
29 | function clean() {
30 | files.forEach((file) => fs.existsSync(file.path) && fs.unlinkSync(file.path));
31 | files = [];
32 | }
33 |
34 | function pushKeys(keys: string[], locales: string[]) {
35 | if (
36 | typeof process.env.VITE_LARAVEL_REACT_I18N_LOCALE !== 'undefined' &&
37 | locales.includes(process.env.VITE_LARAVEL_REACT_I18N_LOCALE)
38 | ) {
39 | const fileName = isPhpLocale
40 | ? `php_${process.env.VITE_LARAVEL_REACT_I18N_LOCALE}`
41 | : process.env.VITE_LARAVEL_REACT_I18N_LOCALE;
42 | keys.push(convertToKeyType(langDirname, fileName));
43 | }
44 |
45 | if (
46 | typeof process.env.VITE_LARAVEL_REACT_I18N_FALLBACK_LOCALE !== 'undefined' &&
47 | locales.includes(process.env.VITE_LARAVEL_REACT_I18N_FALLBACK_LOCALE) &&
48 | process.env.VITE_LARAVEL_REACT_I18N_LOCALE !== process.env.VITE_LARAVEL_REACT_I18N_FALLBACK_LOCALE
49 | ) {
50 | const fileName = isPhpLocale
51 | ? `php_${process.env.VITE_LARAVEL_REACT_I18N_FALLBACK_LOCALE}`
52 | : process.env.VITE_LARAVEL_REACT_I18N_FALLBACK_LOCALE;
53 | keys.push(convertToKeyType(langDirname, fileName));
54 | }
55 | }
56 |
57 | return {
58 | name: 'i18n',
59 | enforce: 'post',
60 | config() {
61 | const keys: string[] = [];
62 |
63 | // Check language directory is exists.
64 | if (!fs.existsSync(langDirname)) {
65 | const msg = [
66 | 'Language directory is not exist, maybe you did not publish the language files with `php artisan lang:publish`.',
67 | 'For more information please visit: https://laravel.com/docs/10.x/localization#publishing-the-language-files'
68 | ];
69 |
70 | msg.map((str) => logger.error(str, { timestamp: true }));
71 | return;
72 | }
73 |
74 | // JSON-file locales.
75 | jsonLocales = locale.getJsonLocale(langDirname);
76 |
77 | if (config?.typeTranslationKeys) {
78 | pushKeys(keys, jsonLocales);
79 | }
80 |
81 | // PHP-file locales.
82 | phpLocales = locale.getPhpLocale(langDirname);
83 |
84 | if (phpLocales.length > 0) {
85 | files = parser(langDirname);
86 | isPhpLocale = true;
87 |
88 | if (config?.typeTranslationKeys) {
89 | pushKeys(keys, phpLocales);
90 | }
91 | } else {
92 | const msg = [
93 | 'Language directory not contain php translations files.',
94 | 'For more information please visit: https://laravel.com/docs/10.x/localization#introduction'
95 | ];
96 |
97 | msg.map((str) => logger.info(str, { timestamp: true }));
98 | }
99 |
100 | if (config?.typeTranslationKeys) {
101 | saveKeyTypeToFile(keys.join('|'), config?.typeDestinationPath);
102 | }
103 | },
104 | buildEnd: clean,
105 | handleHotUpdate(ctx: any) {
106 | const keys: string[] = [];
107 |
108 | if (config?.typeTranslationKeys) {
109 | pushKeys(keys, jsonLocales);
110 | }
111 |
112 | if (isPhpLocale) {
113 | if (/lang\/.*\.php$/.test(ctx.file)) {
114 | files = parser(langDirname);
115 | }
116 |
117 | if (config?.typeTranslationKeys) {
118 | pushKeys(keys, phpLocales);
119 | }
120 | }
121 |
122 | if (config?.typeTranslationKeys) {
123 | saveKeyTypeToFile(keys.join('|'), config?.typeDestinationPath);
124 | }
125 | },
126 | configureServer() {
127 | if (exitHandlersBound) return;
128 |
129 | process.on('exit', clean);
130 | process.on('SIGINT', process.exit);
131 | process.on('SIGTERM', process.exit);
132 | process.on('SIGHUP', process.exit);
133 |
134 | exitHandlersBound = true;
135 | }
136 | };
137 | }
138 |
--------------------------------------------------------------------------------
/tsconfig.client.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "compilerOptions": {
4 | "outDir": "dist"
5 | },
6 | "exclude": [
7 | "node_modules",
8 | "src/plugin/*.ts",
9 | "src/vite.ts"
10 | ],
11 | "include": [
12 | "src/**/*.ts"
13 | ]
14 | }
--------------------------------------------------------------------------------
/tsconfig.commonjs.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.client.json",
3 | "compilerOptions": {
4 | "module": "CommonJS",
5 | "outDir": "dist/cjs"
6 | }
7 | }
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "allowJs": true,
4 | "allowSyntheticDefaultImports": true,
5 | "declaration": true,
6 | "esModuleInterop": true,
7 | "forceConsistentCasingInFileNames": true,
8 | "isolatedModules": true,
9 | "jsx": "react-jsx",
10 | "lib": ["DOM", "DOM.Iterable", "ESNext"],
11 | "module": "ESNext",
12 | "moduleResolution": "Node",
13 | "noFallthroughCasesInSwitch": true,
14 | "resolveJsonModule": true,
15 | "skipLibCheck": true,
16 | "strict": true,
17 | "target": "ES2015",
18 | "types": ["node", "jest", "vite/client"],
19 | },
20 | }
21 |
--------------------------------------------------------------------------------
/tsconfig.plugin.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "compilerOptions": {
4 | "module": "CommonJS",
5 | "outDir": "dist"
6 | },
7 | "include": [
8 | "src/@types/*.d.ts",
9 | "src/plugin/*.ts",
10 | "src/vite.ts"
11 | ]
12 | }
--------------------------------------------------------------------------------
/vite.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 | var i18n = require('./dist/vite');
3 | module.exports = typeof i18n['default'] !== undefined ? i18n['default'] : i18n;
--------------------------------------------------------------------------------