├── .gitattributes ├── .husky ├── pre-commit └── commit-msg ├── .eslintignore ├── .prettierignore ├── .jest ├── setup.ts └── expect.d.ts ├── test ├── index.ts └── ValueTester.tsx ├── .commitlintrc.json ├── .prettierrc.json ├── examples ├── components │ ├── index.js │ ├── LanguageSwitcher.jsx │ └── Translated.jsx ├── phrases │ ├── en.js │ ├── fr.js │ └── index.js └── App.js ├── tsconfig.build.json ├── .lintstagedrc.json ├── src ├── constants.ts ├── types.ts ├── index.ts ├── useT.ts ├── useLocale.ts ├── i18nContext.ts ├── __tests__ │ ├── useLocale.test.tsx │ ├── useT.test.tsx │ ├── I18n.test.tsx │ ├── i18nContext.test.tsx │ ├── enhanceT.test.tsx │ └── T.test.tsx ├── T.tsx ├── I18n.tsx └── enhanceT.ts ├── .babelrc.json ├── jest.config.mjs ├── .gitignore ├── tsconfig.json ├── .github └── dependabot.yml ├── scripts └── test.mjs ├── LICENSE ├── .eslintrc.json ├── .circleci └── config.yml ├── package.json ├── CHANGELOG.md └── README.md /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | yarn lint-staged 2 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | node_modules/ 3 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | node_modules/ 3 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | yarn commitlint --edit "$1" 2 | -------------------------------------------------------------------------------- /.jest/setup.ts: -------------------------------------------------------------------------------- 1 | import '@testing-library/jest-dom'; 2 | -------------------------------------------------------------------------------- /test/index.ts: -------------------------------------------------------------------------------- 1 | export { default as ValueTester } from './ValueTester'; 2 | -------------------------------------------------------------------------------- /.commitlintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["@commitlint/config-conventional"] 3 | } 4 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 100, 3 | "singleQuote": true 4 | } 5 | -------------------------------------------------------------------------------- /examples/components/index.js: -------------------------------------------------------------------------------- 1 | export { default as LanguageSwitcher } from './LanguageSwitcher'; 2 | export { default as Translated } from './Translated'; 3 | -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ".", 3 | "include": ["src"], 4 | "exclude": ["dist", "node_modules", "scripts", "test", "**/__tests__"] 5 | } 6 | -------------------------------------------------------------------------------- /examples/phrases/en.js: -------------------------------------------------------------------------------- 1 | const en = { 2 | hello: 'Hello!', 3 | rabbit: '%{smart_count} rabbit | %{smart_count} rabbits', 4 | }; 5 | 6 | export default en; 7 | -------------------------------------------------------------------------------- /.lintstagedrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "*.{js,ts,tsx}": ["eslint --report-unused-disable-directives", "prettier --write"], 3 | "*.{json,md,yml}": "prettier --write" 4 | } 5 | -------------------------------------------------------------------------------- /examples/phrases/fr.js: -------------------------------------------------------------------------------- 1 | const fr = { 2 | hello: 'Bonjour!', 3 | rabbit: '%{smart_count} lapin |||| %{smart_count} lapins', 4 | }; 5 | 6 | export default fr; 7 | -------------------------------------------------------------------------------- /examples/phrases/index.js: -------------------------------------------------------------------------------- 1 | import en from './en'; 2 | import fr from './fr'; 3 | 4 | const locales = { 5 | en, 6 | fr, 7 | }; 8 | 9 | export default locales; 10 | -------------------------------------------------------------------------------- /src/constants.ts: -------------------------------------------------------------------------------- 1 | export const NO_POLYGLOT_CONTEXT = [ 2 | 'Warning:', 3 | 't is called without Polyglot context.', 4 | 'Perhaps you need to wrap the component in ?', 5 | ].join(' '); 6 | -------------------------------------------------------------------------------- /.babelrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["@babel/preset-env", { "targets": { "node": "current" } }], 4 | ["@babel/preset-react", { "runtime": "automatic" }], 5 | "@babel/preset-typescript" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /test/ValueTester.tsx: -------------------------------------------------------------------------------- 1 | interface ValueTesterProps { 2 | callback(value: T): T; 3 | value: T; 4 | } 5 | 6 | export default function ValueTester({ callback, value }: ValueTesterProps) { 7 | callback(value); 8 | return null; 9 | } 10 | -------------------------------------------------------------------------------- /examples/components/LanguageSwitcher.jsx: -------------------------------------------------------------------------------- 1 | const LanguageSwitcher = ({ onChange }) => ( 2 | 6 | ); 7 | 8 | export default LanguageSwitcher; 9 | -------------------------------------------------------------------------------- /examples/components/Translated.jsx: -------------------------------------------------------------------------------- 1 | import { T } from 'react-polyglot-hooks'; 2 | 3 | const Translated = ({ count }) => ( 4 | <> 5 | 6 | 7 | 8 | ); 9 | 10 | export default Translated; 11 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | import type Polyglot from 'node-polyglot'; 2 | 3 | import type enhanceT from './enhanceT'; 4 | 5 | /** 6 | * The original t function from Polyglot.js. 7 | */ 8 | export type PolyglotT = typeof Polyglot.prototype.t; 9 | 10 | /** 11 | * The t function for translation. 12 | */ 13 | export type tFunction = ReturnType; 14 | -------------------------------------------------------------------------------- /.jest/expect.d.ts: -------------------------------------------------------------------------------- 1 | import type { TestingLibraryMatchers } from '@testing-library/jest-dom/matchers'; 2 | 3 | declare module 'expect' { 4 | interface AsymmetricMatchers extends TestingLibraryMatchers<(str: string) => any, void> {} 5 | interface Matchers, T = unknown> 6 | extends TestingLibraryMatchers<(str: string) => any, R> {} 7 | } 8 | -------------------------------------------------------------------------------- /jest.config.mjs: -------------------------------------------------------------------------------- 1 | const config = { 2 | roots: ['/src'], 3 | collectCoverageFrom: ['src/**/*.{ts,tsx}'], 4 | coverageDirectory: 'coverage', 5 | setupFilesAfterEnv: ['/.jest/setup.ts'], 6 | testEnvironment: 'jsdom', 7 | watchPlugins: ['jest-watch-typeahead/filename', 'jest-watch-typeahead/testname'], 8 | }; 9 | 10 | export default config; 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | 6 | # testing 7 | /coverage 8 | 9 | # distribution 10 | /dist 11 | 12 | # misc 13 | .DS_Store 14 | 15 | # logs 16 | npm-debug.log* 17 | yarn-debug.log* 18 | yarn-error.log* 19 | 20 | # editor config 21 | .idea 22 | .vscode 23 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export type { I18nProps } from './I18n'; 2 | export { default as I18n } from './I18n'; 3 | export type { I18nContextProps } from './i18nContext'; 4 | export type { TProps } from './T'; 5 | export { default as T } from './T'; 6 | export type { tFunction } from './types'; 7 | export { default as useLocale } from './useLocale'; 8 | export { default as useT } from './useT'; 9 | -------------------------------------------------------------------------------- /src/useT.ts: -------------------------------------------------------------------------------- 1 | import { useContext } from 'react'; 2 | 3 | import I18nContext from './i18nContext'; 4 | import type { tFunction } from './types'; 5 | 6 | /** 7 | * A hook to consume the t function. 8 | * 9 | * @returns The t function. 10 | */ 11 | const useT = (): tFunction => { 12 | const { t } = useContext(I18nContext); 13 | return t; 14 | }; 15 | 16 | export default useT; 17 | -------------------------------------------------------------------------------- /src/useLocale.ts: -------------------------------------------------------------------------------- 1 | import { useContext } from 'react'; 2 | 3 | import type { I18nContextProps } from './i18nContext'; 4 | import I18nContext from './i18nContext'; 5 | 6 | /** 7 | * A hook to consume the current locale. 8 | * 9 | * @returns The current active locale. 10 | */ 11 | const useLocale = (): I18nContextProps['locale'] => { 12 | const { locale } = useContext(I18nContext); 13 | return locale; 14 | }; 15 | 16 | export default useLocale; 17 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowSyntheticDefaultImports": true, 4 | "declaration": true, 5 | "esModuleInterop": true, 6 | "forceConsistentCasingInFileNames": true, 7 | "importHelpers": true, 8 | "jsx": "react-jsx", 9 | "lib": ["dom", "dom.iterable", "esnext"], 10 | "module": "commonjs", 11 | "moduleResolution": "node", 12 | "outDir": "./dist", 13 | // Temporarily skipping as Jest types does not match 14 | "skipLibCheck": true, 15 | "strict": true, 16 | "target": "es5" 17 | }, 18 | "include": [".jest/expect.d.ts", "src"], 19 | "exclude": ["dist", "node_modules", "scripts"] 20 | } 21 | -------------------------------------------------------------------------------- /examples/App.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { createRoot } from 'react-dom/client'; 3 | import I18n from 'react-polyglot-hooks'; 4 | 5 | import { LanguageSwitcher, Translated } from './components'; 6 | import Phrases from './phrases'; 7 | 8 | const App = () => { 9 | const [locale, setLocale] = React.useState('en'); 10 | 11 | return ( 12 | 13 | setLocale(value)} /> 14 |
15 | 16 |
17 | ); 18 | }; 19 | 20 | const container = document.getElementById('app'); 21 | const root = createRoot(container); 22 | root.render(); 23 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: npm 4 | directory: '/' 5 | schedule: 6 | interval: weekly 7 | day: saturday 8 | open-pull-requests-limit: 10 9 | assignees: 10 | - pmmmwh 11 | labels: 12 | - dependencies 13 | groups: 14 | babel: 15 | patterns: 16 | - '@babel/*' 17 | commitlint: 18 | patterns: 19 | - '@commitlint/*' 20 | jest: 21 | patterns: 22 | - '@jest/*' 23 | - 'babel-jest' 24 | - 'jest' 25 | - 'jest-*' 26 | typescript-eslint: 27 | patterns: 28 | - '@typescript-eslint/*' 29 | versioning-strategy: widen 30 | -------------------------------------------------------------------------------- /src/i18nContext.ts: -------------------------------------------------------------------------------- 1 | import { createContext } from 'react'; 2 | 3 | import { NO_POLYGLOT_CONTEXT } from './constants'; 4 | import type { tFunction } from './types'; 5 | 6 | function warnWithoutContext(...[key]: Parameters): ReturnType { 7 | if (process.env.NODE_ENV !== 'production') { 8 | console.error(NO_POLYGLOT_CONTEXT); 9 | } 10 | return key as unknown as React.ReactElement; 11 | } 12 | 13 | /** 14 | * The props provided by I18nContext. 15 | */ 16 | export interface I18nContextProps { 17 | locale: string | undefined; 18 | t: tFunction; 19 | } 20 | 21 | /** 22 | * The central store for i18n related components. 23 | */ 24 | const I18nContext = createContext({ 25 | locale: undefined, 26 | t: warnWithoutContext, 27 | }); 28 | 29 | export default I18nContext; 30 | -------------------------------------------------------------------------------- /src/__tests__/useLocale.test.tsx: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from '@jest/globals'; 2 | import { renderHook } from '@testing-library/react'; 3 | import type { ReactNode } from 'react'; 4 | 5 | import { I18n, useLocale } from '..'; 6 | 7 | describe('Use Locale', () => { 8 | it('should return undefined without context', () => { 9 | const { result } = renderHook(() => useLocale()); 10 | expect(result.current).toBe(undefined); 11 | }); 12 | 13 | it('should return the locale string with context', () => { 14 | const wrapper = ({ children }: { children: ReactNode }) => ( 15 | 16 | {children} 17 | 18 | ); 19 | const { result } = renderHook(() => useLocale(), { wrapper }); 20 | expect(typeof result.current).toBe('string'); 21 | expect(result.current).toBe('en'); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /scripts/test.mjs: -------------------------------------------------------------------------------- 1 | // This script have been bootstrapped from the CRA repo and trimmed down to fit library use cases 2 | import process from 'node:process'; 3 | 4 | import jest from 'jest'; 5 | 6 | // Do this as the first thing so that any code reading it knows the right env. 7 | process.env.BABEL_ENV = 'test'; 8 | process.env.NODE_ENV = 'test'; 9 | 10 | // Makes the script crash on unhandled rejections instead of silently 11 | // ignoring them. In the future, promise rejections that are not handled will 12 | // terminate the Node.js process with a non-zero exit code. 13 | process.on('unhandledRejection', (err) => { 14 | throw err; 15 | }); 16 | 17 | let argv = process.argv.slice(2); 18 | 19 | // Watch unless on CI or explicitly running all tests 20 | if ( 21 | !process.env.CI && 22 | argv.indexOf('--watchAll') === -1 && 23 | argv.indexOf('--watchAll=false') === -1 24 | ) { 25 | argv.push('--watch'); 26 | } 27 | 28 | void jest.run(argv); 29 | -------------------------------------------------------------------------------- /src/__tests__/useT.test.tsx: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from '@jest/globals'; 2 | import { renderHook } from '@testing-library/react'; 3 | import type { ReactNode } from 'react'; 4 | 5 | import { I18n, useT } from '..'; 6 | 7 | describe('Use T', () => { 8 | it('should return the fallback function without context', () => { 9 | const { result } = renderHook(() => useT()); 10 | expect(typeof result.current).toBe('function'); 11 | expect(result.current.toString()).toContain('function warnWithoutContext'); 12 | }); 13 | 14 | it('should return the t function with context', () => { 15 | const wrapper = ({ children }: { children: ReactNode }) => ( 16 | 17 | {children} 18 | 19 | ); 20 | const { result } = renderHook(() => useT(), { wrapper }); 21 | expect(typeof result.current).toBe('function'); 22 | expect(result.current.toString()).toContain('function t'); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /src/T.tsx: -------------------------------------------------------------------------------- 1 | import type { InterpolationOptions } from 'node-polyglot'; 2 | 3 | import useT from './useT'; 4 | 5 | /** 6 | * Props accepted by the T component. 7 | */ 8 | export interface TProps { 9 | /** A count used for pluralization. */ 10 | count?: number; 11 | /** A fallback phrase to render when the provided key does not resolve. */ 12 | fallback?: string; 13 | /** A key-value map of components or strings to be interpolated. */ 14 | interpolations?: InterpolationOptions; 15 | /** The key of the phrase to render. */ 16 | phrase: string; 17 | } 18 | 19 | /** 20 | * A component to render a translated string. 21 | */ 22 | const T = ({ count, fallback, interpolations, phrase }: TProps) => { 23 | const t = useT(); 24 | 25 | const tOptions = { 26 | // Check for truthy prop values before assigning 27 | // This is done to prevent altering the specific options 28 | ...(fallback && { _: fallback }), 29 | ...(count && { smart_count: count }), 30 | ...interpolations, 31 | }; 32 | 33 | return t(phrase, tOptions); 34 | }; 35 | 36 | export default T; 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Michael Mok 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "env": { 4 | "browser": true, 5 | "es2018": true 6 | }, 7 | "extends": [ 8 | "eslint:recommended", 9 | "plugin:react/recommended", 10 | "plugin:react/jsx-runtime", 11 | "plugin:react-hooks/recommended", 12 | "plugin:prettier/recommended" 13 | ], 14 | "parserOptions": { 15 | "ecmaVersion": 2018, 16 | "sourceType": "module" 17 | }, 18 | "plugins": ["simple-import-sort"], 19 | "rules": { 20 | "react/prop-types": "off", 21 | "simple-import-sort/imports": "error", 22 | "simple-import-sort/exports": "error" 23 | }, 24 | "settings": { 25 | "react": { 26 | "version": "detect" 27 | } 28 | }, 29 | "overrides": [ 30 | { 31 | "files": ["**/*.ts?(x)"], 32 | "parser": "@typescript-eslint/parser", 33 | "extends": ["plugin:@typescript-eslint/recommended", "plugin:prettier/recommended"], 34 | "rules": { 35 | "@typescript-eslint/ban-ts-comment": [ 36 | "warn", 37 | { 38 | "ts-expect-error": "allow-with-description", 39 | "ts-ignore": "allow-with-description" 40 | } 41 | ], 42 | "@typescript-eslint/consistent-type-imports": "error", 43 | "@typescript-eslint/no-unused-vars": [ 44 | "error", 45 | { 46 | "argsIgnorePattern": "^_", 47 | "ignoreRestSiblings": true 48 | } 49 | ] 50 | } 51 | } 52 | ] 53 | } 54 | -------------------------------------------------------------------------------- /src/I18n.tsx: -------------------------------------------------------------------------------- 1 | import type { PolyglotOptions } from 'node-polyglot'; 2 | import Polyglot from 'node-polyglot'; 3 | import { useMemo } from 'react'; 4 | 5 | import enhanceT from './enhanceT'; 6 | import I18nContext from './i18nContext'; 7 | 8 | /** 9 | * Props accepted by the I18n component. 10 | */ 11 | export interface I18nProps extends PolyglotOptions { 12 | /** The children to consume i18n props. */ 13 | children?: React.ReactNode; 14 | /** The current locale. */ 15 | locale: string; 16 | /** The current key-value map of phrases. */ 17 | phrases: object; 18 | } 19 | 20 | /** 21 | * A component to allow consumption of i18n props from any nested children. 22 | */ 23 | const I18n = ({ 24 | children, 25 | locale, 26 | phrases, 27 | allowMissing, 28 | interpolation, 29 | onMissingKey, 30 | pluralRules, 31 | }: I18nProps) => { 32 | const polyglot = useMemo( 33 | // Create a new instance on every change 34 | // This make sure we always consume the latest phrases and Polyglot context 35 | () => 36 | new Polyglot({ 37 | locale, 38 | phrases, 39 | allowMissing, 40 | interpolation, 41 | onMissingKey, 42 | pluralRules, 43 | }), 44 | [locale, phrases, allowMissing, interpolation, onMissingKey, pluralRules], 45 | ); 46 | 47 | const polyglotContext = useMemo( 48 | () => ({ 49 | locale: polyglot.locale(), 50 | t: enhanceT(polyglot.t.bind(polyglot)), 51 | }), 52 | [polyglot], 53 | ); 54 | 55 | return ( 56 | 57 | {polyglotContext.locale && children} 58 | 59 | ); 60 | }; 61 | 62 | export default I18n; 63 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2.1 2 | 3 | anchors: 4 | - &node-version-enum 5 | - '22.2' 6 | 7 | commands: 8 | restore_yarn_cache: 9 | steps: 10 | - restore_cache: 11 | name: Restore Dependency Cache 12 | keys: 13 | - dependencies-v1-{{ checksum "yarn.lock" }} 14 | - dependencies-v1- 15 | 16 | executors: 17 | node: 18 | parameters: 19 | version: 20 | default: '22.2' 21 | type: string 22 | docker: 23 | - image: cimg/node:<< parameters.version >> 24 | auth: 25 | username: $DOCKER_LOGIN 26 | password: $DOCKER_PASSWORD 27 | working_directory: ~/repo 28 | 29 | orbs: 30 | codecov: codecov/codecov@4.1.0 31 | 32 | jobs: 33 | setup: 34 | executor: node 35 | steps: 36 | - checkout 37 | - restore_yarn_cache 38 | - run: 39 | name: Install Dependencies 40 | command: yarn install --frozen-lockfile 41 | - save_cache: 42 | name: Save Dependency Cache 43 | key: dependencies-v1-{{ checksum "yarn.lock" }} 44 | paths: 45 | - node_modules 46 | 47 | build: 48 | executor: node 49 | steps: 50 | - checkout 51 | - restore_yarn_cache 52 | - run: 53 | name: Build Library 54 | command: yarn build 55 | 56 | quality: 57 | executor: node 58 | steps: 59 | - checkout 60 | - restore_yarn_cache 61 | - run: 62 | name: Setup Reporting Environment 63 | command: mkdir -p reports/eslint 64 | - run: 65 | name: Check Code Quality with ESLint 66 | command: yarn lint --format junit --output-file $CIRCLE_WORKING_DIRECTORY/reports/eslint/results.xml 67 | - run: 68 | name: Check Code Style with Prettier 69 | command: yarn format:check 70 | - store_test_results: 71 | path: reports 72 | 73 | test: 74 | parameters: 75 | node-version: 76 | default: lts 77 | type: string 78 | executor: 79 | name: node 80 | version: << parameters.node-version >> 81 | steps: 82 | - checkout 83 | - restore_yarn_cache 84 | - run: 85 | name: Run tests and generate coverage report 86 | command: yarn test --coverage 87 | - codecov/upload 88 | 89 | workflows: 90 | version: 2 91 | pipeline: 92 | jobs: 93 | - setup: 94 | context: DOCKER_CREDENTIALS 95 | - build: 96 | context: DOCKER_CREDENTIALS 97 | requires: 98 | - setup 99 | - quality: 100 | context: DOCKER_CREDENTIALS 101 | requires: 102 | - setup 103 | - test: 104 | context: DOCKER_CREDENTIALS 105 | matrix: 106 | parameters: 107 | node-version: *node-version-enum 108 | name: test/node:<< matrix.node-version >> 109 | requires: 110 | - setup 111 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-polyglot-hooks", 3 | "version": "0.5.0", 4 | "description": "Hooks for using Polyglot.js with React.", 5 | "keywords": [ 6 | "react", 7 | "react-hooks", 8 | "hooks", 9 | "airbnb", 10 | "polyglot", 11 | "i18n", 12 | "internationalization", 13 | "internationalisation", 14 | "translate", 15 | "translation" 16 | ], 17 | "author": "Michael Mok", 18 | "main": "dist/index.js", 19 | "types": "dist/index.d.ts", 20 | "files": [ 21 | "dist" 22 | ], 23 | "license": "MIT", 24 | "repository": { 25 | "type": "git", 26 | "url": "https://github.com/pmmmwh/react-polyglot-hooks.git" 27 | }, 28 | "bugs": { 29 | "url": "https://github.com/pmmmwh/react-polyglot-hooks/issues" 30 | }, 31 | "homepage": "https://github.com/pmmmwh/react-polyglot-hooks#readme", 32 | "scripts": { 33 | "build": "tsc -p tsconfig.build.json", 34 | "lint": "eslint --report-unused-disable-directives \"**/*.{js,mjs,jsx,ts,tsx}\"", 35 | "lint:fix": "yarn lint --fix", 36 | "format": "prettier --write \"**/*.{js,mjs,jsx,ts,tsx,json,md,yml}\"", 37 | "format:check": "prettier --check \"**/*.{js,mjs,jsx,ts,tsx,json,md,yml}\"", 38 | "prebuild": "rimraf dist", 39 | "prepare": "husky", 40 | "release": "yarn build && standard-version", 41 | "test": "node scripts/test.mjs", 42 | "type-check": "tsc --noEmit --pretty" 43 | }, 44 | "dependencies": { 45 | "node-polyglot": "^2.4.0", 46 | "tslib": "^2.0.0" 47 | }, 48 | "devDependencies": { 49 | "@babel/core": "^7.11.0", 50 | "@babel/preset-env": "^7.11.0", 51 | "@babel/preset-react": "^7.10.4", 52 | "@babel/preset-typescript": "^7.10.4", 53 | "@commitlint/cli": "^20.0.0", 54 | "@commitlint/config-conventional": "^20.0.0", 55 | "@jest/globals": "^29.5.0", 56 | "@testing-library/jest-dom": "^6.1.2", 57 | "@testing-library/react": "^15.0.2", 58 | "@types/node": "^24.3.0", 59 | "@types/node-polyglot": "^2.4.1", 60 | "@types/react": "^18.0.1", 61 | "@typescript-eslint/eslint-plugin": "^8.0.0", 62 | "@typescript-eslint/parser": "^8.0.0", 63 | "babel-jest": "^29.0.2", 64 | "eslint": "^8.36.0", 65 | "eslint-config-prettier": "^10.0.1", 66 | "eslint-plugin-prettier": "^5.0.0", 67 | "eslint-plugin-react": "^7.20.5", 68 | "eslint-plugin-react-hooks": "^7.0.0", 69 | "eslint-plugin-simple-import-sort": "^12.0.0", 70 | "husky": "^9.0.11", 71 | "jest": "^29.0.2", 72 | "jest-environment-jsdom": "^29.0.2", 73 | "jest-watch-typeahead": "^2.0.0", 74 | "lint-staged": "^15.0.1", 75 | "prettier": "^3.0.3", 76 | "react": "^18.0.0", 77 | "react-dom": "^18.0.0", 78 | "rimraf": "^6.0.1", 79 | "standard-version": "^9.0.0", 80 | "typescript": "5.9.3" 81 | }, 82 | "peerDependencies": { 83 | "react": "^16.8.0 || 17.x || 18.x", 84 | "react-dom": "^16.8.0 || 17.x || 18.x" 85 | }, 86 | "engines": { 87 | "node": ">=12.18" 88 | }, 89 | "packageManager": "yarn@1.22.22+sha1.ac34549e6aa8e7ead463a7407e1c7390f61a6610", 90 | "sideEffects": false 91 | } 92 | -------------------------------------------------------------------------------- /src/enhanceT.ts: -------------------------------------------------------------------------------- 1 | import { cloneElement, isValidElement } from 'react'; 2 | 3 | import type { PolyglotT } from './types'; 4 | 5 | /** 6 | * A pseudo-JSX string interpolation identifier. 7 | */ 8 | const IDENTIFIER = /<(\d+)\/>/; 9 | 10 | /** 11 | * A function to enhance Polyglot.js to allow React component interpolations. 12 | * 13 | * @param originalT The original t function from Polyglot.js. 14 | * @returns The enhanced t function. 15 | */ 16 | const enhanceT = (originalT: PolyglotT) => { 17 | /** 18 | * The t function for translation. 19 | * 20 | * @param key The key of one translate phrase. 21 | * @param interpolations The nodes to be interpolated to the phrase. 22 | * @returns A string, or an array of components and strings. 23 | */ 24 | // The overload included here is to aid code auto-completion 25 | function t( 26 | key: Parameters[0], 27 | interpolations?: Parameters[1], 28 | ): React.ReactElement; 29 | // We use a named function here to aid debugging 30 | // ReactNode includes all React render-ables, including arrays 31 | function t(...[key, interpolations]: Parameters): React.ReactElement { 32 | if (interpolations === undefined || typeof interpolations === 'number') { 33 | return originalT(key, interpolations) as unknown as React.ReactElement; 34 | } else { 35 | // ReactElement used because cloneElement requires a proper ReactElement 36 | const elementCache: React.ReactElement[] = []; 37 | Object.keys(interpolations).forEach((key) => { 38 | // Store all JSX elements into an array cache for processing later 39 | if (isValidElement(interpolations[key])) { 40 | elementCache.push(interpolations[key]); 41 | interpolations[key] = `<${elementCache.length - 1}/>`; 42 | } 43 | }); 44 | 45 | const tString = originalT(key, interpolations); 46 | // We can safely return if we don't need to do any element interpolation 47 | if (!elementCache.length) { 48 | return tString as unknown as React.ReactElement; 49 | } 50 | 51 | // Split the string into chunks of strings and interpolation indices 52 | const tChunks = tString.split(IDENTIFIER); 53 | // Move the leading string part into the render array and pop it 54 | const renderItems: Array = tChunks.splice(0, 1); 55 | for (let i = 0; i < Math.ceil(tChunks.length); i += 1) { 56 | const [index, trailingString] = tChunks.splice(0, 2); 57 | const currentIndex = parseInt(index, 10); 58 | const currentElement = elementCache[currentIndex]; 59 | 60 | // Interpolate the element 61 | renderItems.push( 62 | cloneElement( 63 | currentElement, 64 | // We need a unique key when rendering an array 65 | { key: currentIndex }, 66 | currentElement.props.children, 67 | ), 68 | ); 69 | 70 | // Interpolate any trailing string 71 | if (trailingString) { 72 | renderItems.push(trailingString); 73 | } 74 | } 75 | 76 | return renderItems as unknown as React.ReactElement; 77 | } 78 | } 79 | 80 | return t; 81 | }; 82 | 83 | export default enhanceT; 84 | -------------------------------------------------------------------------------- /src/__tests__/I18n.test.tsx: -------------------------------------------------------------------------------- 1 | import { describe, expect, it, jest } from '@jest/globals'; 2 | import { render } from '@testing-library/react'; 3 | 4 | import { ValueTester } from '../../test'; 5 | import type { I18nProps } from '../I18n'; 6 | import I18n from '../I18n'; 7 | import I18nContext from '../i18nContext'; 8 | import type { tFunction } from '../types'; 9 | 10 | describe('I18n Provider', () => { 11 | it('should render without crashing', () => { 12 | render(); 13 | }); 14 | 15 | it('should provide a locale from Polyglot', () => { 16 | const tree = ( 17 | 18 | 19 | {({ locale }) => Received: {locale}} 20 | 21 | 22 | ); 23 | const { getByText } = render(tree); 24 | expect(getByText(/^Received:/).textContent).toBe('Received: en'); 25 | }); 26 | 27 | it('should provide default locale (en) from Polyglot', () => { 28 | const tree = ( 29 | 30 | 31 | {({ locale }) => Received: {locale}} 32 | 33 | 34 | ); 35 | const { getByText } = render(tree); 36 | expect(getByText(/^Received:/).textContent).toBe('Received: en'); 37 | }); 38 | 39 | it('should provide a working t function from Polyglot', () => { 40 | const readValue = jest.fn((v: tFunction) => v); 41 | const tree = ( 42 | 43 | 44 | {({ t }) => } 45 | 46 | 47 | ); 48 | render(tree); 49 | const calledValue = readValue.mock.lastCall?.[0]; 50 | expect(typeof calledValue).toBe('function'); 51 | expect(calledValue!.toString()).toContain('function t'); 52 | expect(calledValue!('phrase')).toBe('Message'); 53 | }); 54 | 55 | it('should update after a change in locale', () => { 56 | const Tree = ({ locale }: Pick) => ( 57 | 58 | 59 | {({ locale }) => Received: {locale}} 60 | 61 | 62 | ); 63 | const { getByText, rerender } = render(); 64 | expect(getByText(/^Received:/).textContent).toBe('Received: en'); 65 | 66 | rerender(); 67 | expect(getByText(/^Received:/).textContent).toBe('Received: zh'); 68 | }); 69 | 70 | it('should update after a change in phrases', () => { 71 | const readValue = jest.fn((v: tFunction) => v); 72 | const Tree = ({ phrases }: Pick) => ( 73 | 74 | 75 | {({ t }) => } 76 | 77 | 78 | ); 79 | const { rerender } = render(); 80 | const firstValue = readValue.mock.lastCall?.[0]; 81 | expect(typeof firstValue).toBe('function'); 82 | expect(firstValue!('test')).toBe('Test'); 83 | 84 | rerender(); 85 | const secondValue = readValue.mock.lastCall?.[0]; 86 | expect(typeof secondValue).toBe('function'); 87 | expect(secondValue!('test')).toBe('Expected'); 88 | }); 89 | }); 90 | -------------------------------------------------------------------------------- /src/__tests__/i18nContext.test.tsx: -------------------------------------------------------------------------------- 1 | import { afterEach, beforeEach, describe, expect, it } from '@jest/globals'; 2 | import { render } from '@testing-library/react'; 3 | import type { ReactElement } from 'react'; 4 | 5 | import { ValueTester } from '../../test'; 6 | import { NO_POLYGLOT_CONTEXT } from '../constants'; 7 | import type { I18nContextProps } from '../i18nContext'; 8 | import I18nContext from '../i18nContext'; 9 | import type { tFunction } from '../types'; 10 | 11 | describe('I18n Context', () => { 12 | const originalConsoleError = console.error; 13 | 14 | let consoleOutput: string[] = []; 15 | beforeEach(() => { 16 | console.error = (...args: string[]): void => { 17 | args.forEach((arg) => consoleOutput.push(arg)); 18 | }; 19 | }); 20 | 21 | afterEach(() => { 22 | consoleOutput = []; 23 | console.error = originalConsoleError; 24 | }); 25 | 26 | it('should only pass locale and t to children via context', () => { 27 | const readValue = jest.fn((v: I18nContextProps) => v); 28 | const tree = ( 29 | undefined as unknown as ReactElement, 33 | }} 34 | > 35 | 36 | {(values) => } 37 | 38 | 39 | ); 40 | render(tree); 41 | const calledValue = readValue.mock.lastCall?.[0]; 42 | expect(typeof calledValue).toBe('object'); 43 | 44 | const valueKeys = Object.keys(calledValue!); 45 | expect(valueKeys).toHaveLength(2); 46 | expect(valueKeys).toStrictEqual(['locale', 't']); 47 | }); 48 | 49 | it('should provide undefined locale without context', () => { 50 | const readValue = jest.fn((v?: string) => v); 51 | const tree = ( 52 | 53 | {({ locale }) => } 54 | 55 | ); 56 | render(tree); 57 | const calledValue = readValue.mock.lastCall?.[0]; 58 | expect(calledValue).toBe(undefined); 59 | }); 60 | 61 | it('should provide a fallback t function without context', () => { 62 | const readValue = jest.fn((v: tFunction) => v); 63 | const tree = ( 64 | 65 | {({ t }) => } 66 | 67 | ); 68 | render(tree); 69 | const calledValue = readValue.mock.lastCall?.[0]; 70 | expect(typeof calledValue).toBe('function'); 71 | expect(calledValue!.toString()).toContain('function warnWithoutContext'); 72 | expect(calledValue!('phrase')).toBe('phrase'); 73 | expect(consoleOutput).toHaveLength(1); 74 | expect(consoleOutput[0]).toBe(NO_POLYGLOT_CONTEXT); 75 | }); 76 | 77 | describe('In Production', () => { 78 | const originalNodeEnv = process.env.NODE_ENV; 79 | 80 | beforeAll(() => { 81 | process.env.NODE_ENV = 'production'; 82 | }); 83 | 84 | afterAll(() => { 85 | process.env.NODE_ENV = originalNodeEnv; 86 | }); 87 | 88 | it('should silence fallback t function warnings', () => { 89 | const readValue = jest.fn((v: tFunction) => v); 90 | const tree = ( 91 | 92 | {({ t }) => } 93 | 94 | ); 95 | render(tree); 96 | readValue.mock.lastCall![0]('phrase'); 97 | expect(consoleOutput).toHaveLength(0); 98 | }); 99 | }); 100 | }); 101 | -------------------------------------------------------------------------------- /src/__tests__/enhanceT.test.tsx: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from '@jest/globals'; 2 | import { render } from '@testing-library/react'; 3 | import Polyglot from 'node-polyglot'; 4 | 5 | import enhanceT from '../enhanceT'; 6 | 7 | describe('Enhance T', () => { 8 | const polyglot = new Polyglot({ 9 | locale: 'en', 10 | phrases: { 11 | phrase: 'Message', 12 | count: 'Count: %{smart_count}', 13 | interpolation: 'Interpolated: %{message}', 14 | rich_text: '%{component}', 15 | rich_text_leading: 'Leading %{component}', 16 | rich_text_trailing: '%{component} Trailing', 17 | }, 18 | }); 19 | 20 | const enhancedT = enhanceT(polyglot.t.bind(polyglot)); 21 | 22 | it('should return a phrase', () => { 23 | const callResult = enhancedT('phrase'); 24 | expect(callResult).toBe('Message'); 25 | }); 26 | 27 | it('should interpolate object values to a phrase', () => { 28 | const callResult = enhancedT('interpolation', { 29 | message: 'Success!', 30 | }); 31 | expect(callResult).toBe('Interpolated: Success!'); 32 | }); 33 | 34 | it('should interpolate number to a phrase', () => { 35 | const callResult = enhancedT('count', 1); 36 | expect(callResult).toBe('Count: 1'); 37 | }); 38 | 39 | it('should interpolate smart count to a phrase', () => { 40 | const callResult = enhancedT('count', { smart_count: 1 }); 41 | expect(callResult).toBe('Count: 1'); 42 | }); 43 | 44 | // Because we are testing against an array of ReactNodes here, 45 | // we need a full render to allow effective and meaningful comparisons 46 | describe('when component interpolations are received', () => { 47 | it('should interpolate component to a phrase', () => { 48 | const testComponent = Component; 49 | const { getByTestId } = render(enhancedT('rich_text', { component: testComponent })); 50 | expect(getByTestId('ComponentID')).toBeInTheDocument(); 51 | expect(document.querySelector('b[data-testid="ComponentID"]')).toBeInTheDocument(); 52 | expect(getByTestId('ComponentID').textContent).toBe('Component'); 53 | }); 54 | 55 | it('should interpolate component with leading string to a phrase', () => { 56 | const testComponent = Component; 57 | const { getByText, getByTestId } = render( 58 | enhancedT('rich_text_leading', { component: testComponent }), 59 | ); 60 | expect(getByText(/^Leading/)).toBeInTheDocument(); 61 | expect(getByTestId('ComponentID')).toBeInTheDocument(); 62 | expect(getByText(/^Leading/)).toContainElement(getByTestId('ComponentID')); 63 | expect(getByText(/^Leading/).textContent).toBe('Leading Component'); 64 | }); 65 | 66 | it('should interpolate component with trailing string to a phrase', () => { 67 | const testComponent = Component; 68 | const { getByText, getByTestId } = render( 69 | enhancedT('rich_text_trailing', { component: testComponent }), 70 | ); 71 | expect(getByText(/Trailing$/)).toBeInTheDocument(); 72 | expect(getByTestId('ComponentID')).toBeInTheDocument(); 73 | expect(getByText(/Trailing$/)).toContainElement(getByTestId('ComponentID')); 74 | expect(getByText(/Trailing$/).textContent).toBe('Component Trailing'); 75 | }); 76 | }); 77 | 78 | describe('when the called phrase is not found', () => { 79 | const originalConsoleError = console.error; 80 | 81 | let consoleOutput: string[] = []; 82 | beforeEach(() => { 83 | console.error = (...args: string[]): void => { 84 | args.forEach((arg) => consoleOutput.push(arg)); 85 | }; 86 | }); 87 | 88 | afterEach(() => { 89 | consoleOutput = []; 90 | console.error = originalConsoleError; 91 | }); 92 | 93 | it('should return the key and emit a warning', () => { 94 | const callResult = enhancedT('unavailable'); 95 | expect(callResult).toBe('unavailable'); 96 | expect(consoleOutput).toHaveLength(1); 97 | expect(consoleOutput[0]).toBe('Warning: Missing translation for key: "unavailable"'); 98 | }); 99 | 100 | it('should not interpolate without a fallback', () => { 101 | const callResult = enhancedT('unavailable', { 102 | message: 'Failed!', 103 | }); 104 | expect(callResult).not.toContain('Failed!'); 105 | expect(callResult).toBe('unavailable'); 106 | }); 107 | 108 | it('should interpolate to the fallback when available', () => { 109 | const callResult = enhancedT('unavailable', { 110 | _: 'Fallback: %{message}', 111 | message: 'Success!', 112 | }); 113 | expect(callResult).not.toContain('unavailable'); 114 | expect(callResult).toBe('Fallback: Success!'); 115 | }); 116 | }); 117 | }); 118 | -------------------------------------------------------------------------------- /src/__tests__/T.test.tsx: -------------------------------------------------------------------------------- 1 | import { afterEach, beforeEach, describe, expect, it } from '@jest/globals'; 2 | import { render } from '@testing-library/react'; 3 | 4 | import { I18n, T } from '..'; 5 | 6 | describe('T Component', () => { 7 | const originalConsoleWarn = console.warn; 8 | const originalConsoleError = console.error; 9 | 10 | let consoleOutput: string[] = []; 11 | beforeEach(() => { 12 | console.error = (...args: string[]): void => { 13 | args.forEach((arg) => consoleOutput.push(arg)); 14 | }; 15 | console.warn = (...args: string[]): void => { 16 | args.forEach((arg) => consoleOutput.push(arg)); 17 | }; 18 | }); 19 | 20 | afterEach(() => { 21 | consoleOutput = []; 22 | console.error = originalConsoleError; 23 | console.warn = originalConsoleWarn; 24 | }); 25 | 26 | describe('when a phrase cannot be found', () => { 27 | it('should render the key', () => { 28 | const tree = ( 29 | 30 | 31 | 32 | ); 33 | const { getByText, queryByText } = render(tree); 34 | expect(queryByText('phrase')).not.toBeInTheDocument(); 35 | expect(queryByText('Message')).not.toBeInTheDocument(); 36 | expect(getByText('unavailable')).toBeInTheDocument(); 37 | }); 38 | 39 | it('should render the fallback when available', () => { 40 | const tree = ( 41 | 42 | 43 | 44 | ); 45 | const { getByText, queryByText } = render(tree); 46 | expect(queryByText('phrase')).not.toBeInTheDocument(); 47 | expect(queryByText('Message')).not.toBeInTheDocument(); 48 | expect(getByText('Fallback')).toBeInTheDocument(); 49 | expect(consoleOutput).toHaveLength(0); 50 | }); 51 | 52 | it('should allow interpolations to override fallback', () => { 53 | const tree = ( 54 | 55 | 56 | 57 | ); 58 | const { getByText, queryByText } = render(tree); 59 | expect(queryByText('Incorrect')).not.toBeInTheDocument(); 60 | expect(getByText('Fallback')).toBeInTheDocument(); 61 | }); 62 | }); 63 | 64 | describe('when a phrase can be found', () => { 65 | it('should render without crashing', () => { 66 | const tree = ( 67 | 68 | 69 | 70 | ); 71 | const { getByText } = render(tree); 72 | expect(getByText('Message')).toBeInTheDocument(); 73 | }); 74 | 75 | it('should interpolate values', () => { 76 | const tree = ( 77 | 78 | 79 | 80 | ); 81 | const { getByText } = render(tree); 82 | expect(getByText(/^Interpolated:/)).toBeInTheDocument(); 83 | expect(getByText(/^Interpolated:/).textContent).toBe('Interpolated: Success!'); 84 | }); 85 | 86 | it('should not interpolate number as smart count', () => { 87 | const tree = ( 88 | 89 | {/* @ts-expect-error Unsupported use case*/} 90 | 91 | 92 | ); 93 | const { getByText } = render(tree); 94 | expect(getByText(/^Interpolated:/)).toBeInTheDocument(); 95 | expect(getByText(/^Interpolated:/).textContent).toBe('Interpolated: %{smart_count}'); 96 | }); 97 | 98 | it('should interpolate count', () => { 99 | const tree = ( 100 | 101 | 102 | 103 | ); 104 | const { getByText } = render(tree); 105 | expect(getByText(/^Interpolated:/)).toBeInTheDocument(); 106 | expect(getByText(/^Interpolated:/).textContent).toBe('Interpolated: 1'); 107 | }); 108 | 109 | it('should allow interpolations to override count', () => { 110 | const tree = ( 111 | 112 | 113 | 114 | ); 115 | const { getByText, queryByText } = render(tree); 116 | expect(queryByText(/1$/)).not.toBeInTheDocument(); 117 | expect(getByText(/^Interpolated:/)).toBeInTheDocument(); 118 | expect(getByText(/^Interpolated:/).textContent).toBe('Interpolated: 2'); 119 | }); 120 | }); 121 | }); 122 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. 4 | 5 | ## [0.5.0](https://github.com/pmmmwh/react-polyglot-hooks/compare/v0.4.0...v0.5.0) (2020-08-01) 6 | 7 | ### ⚠ BREAKING CHANGES 8 | 9 | - **deps:** bump all deps (#311) 10 | 11 | - **deps:** bump all deps ([#311](https://github.com/pmmmwh/react-polyglot-hooks/issues/311)) ([a18e202](https://github.com/pmmmwh/react-polyglot-hooks/commit/a18e20289253cbe53380544aa929a2ec638a97c8)) 12 | 13 | ## [0.4.0](https://github.com/pmmmwh/react-polyglot-hooks/compare/v0.3.1...v0.4.0) (2020-04-18) 14 | 15 | ### ⚠ BREAKING CHANGES 16 | 17 | - remove number interpolations for T component 18 | - bump minimum version of node to 10 19 | 20 | ### Features 21 | 22 | - bump minimum version of node to 10 ([09d19a5](https://github.com/pmmmwh/react-polyglot-hooks/commit/09d19a500f81821c75541ca6312cb01c7776e078)) 23 | - remove number interpolations for T component ([be1a8c2](https://github.com/pmmmwh/react-polyglot-hooks/commit/be1a8c2418e3742ba433dfd2ba230bd35ba01acd)) 24 | 25 | ### [0.3.1](https://github.com/pmmmwh/react-polyglot-hooks/compare/v0.3.0...v0.3.1) (2019-10-10) 26 | 27 | ### Features 28 | 29 | - accept the pluralRules prop with I18n ([0a17568](https://github.com/pmmmwh/react-polyglot-hooks/commit/0a17568)) 30 | - add types to support custom plural rules ([1e9a17a](https://github.com/pmmmwh/react-polyglot-hooks/commit/1e9a17a)) 31 | - make pluralRules optional ([bd71c66](https://github.com/pmmmwh/react-polyglot-hooks/commit/bd71c66)) 32 | 33 | ## [0.3.0](https://github.com/pmmmwh/react-polyglot-hooks/compare/v0.3.0-0...v0.3.0) (2019-09-05) 34 | 35 | ## [0.3.0-0](https://github.com/pmmmwh/react-polyglot-hooks/compare/v0.2.0...v0.3.0-0) (2019-09-02) 36 | 37 | ### Bug Fixes 38 | 39 | - **t:** add type casting for fallback function return ([8e8e975](https://github.com/pmmmwh/react-polyglot-hooks/commit/8e8e975)) 40 | - **t:** return ReactElement instead of ReactNode ([1eb60ca](https://github.com/pmmmwh/react-polyglot-hooks/commit/1eb60ca)) 41 | - rename t type to PolyglotT ([786ed00](https://github.com/pmmmwh/react-polyglot-hooks/commit/786ed00)) 42 | 43 | ### Features 44 | 45 | - **i18n:** remove children.only but only render on polyglot mounted ([6e3eea8](https://github.com/pmmmwh/react-polyglot-hooks/commit/6e3eea8)) 46 | - **t:** add a count prop as a shorthand to use smart_count ([0524125](https://github.com/pmmmwh/react-polyglot-hooks/commit/0524125)) 47 | - **t:** add function overload to aid code auto completion ([a520de4](https://github.com/pmmmwh/react-polyglot-hooks/commit/a520de4)) 48 | - **t:** implement polyglot enhancer to allow component interpolation ([7936f72](https://github.com/pmmmwh/react-polyglot-hooks/commit/7936f72)) 49 | - **t:** update function types to enhanced t ([163a033](https://github.com/pmmmwh/react-polyglot-hooks/commit/163a033)) 50 | - **t:** utilize new enhancer and type in i18n ([f5abaaf](https://github.com/pmmmwh/react-polyglot-hooks/commit/f5abaaf)) 51 | - export PolyglotT type ([e582380](https://github.com/pmmmwh/react-polyglot-hooks/commit/e582380)) 52 | 53 | ## [0.2.0](https://github.com/pmmmwh/react-polyglot-hooks/compare/v0.1.3...v0.2.0) (2019-08-28) 54 | 55 | ### Bug Fixes 56 | 57 | - **i18n:** allow consumption of t options ([0e9371b](https://github.com/pmmmwh/react-polyglot-hooks/commit/0e9371b)) 58 | - **t:** BREAKING - rename options to interpolations ([88d58df](https://github.com/pmmmwh/react-polyglot-hooks/commit/88d58df)) 59 | 60 | ### Features 61 | 62 | - **t:** add a fallback prop for easier consumption ([f919872](https://github.com/pmmmwh/react-polyglot-hooks/commit/f919872)) 63 | 64 | ### [0.1.3](https://github.com/pmmmwh/react-polyglot-hooks/compare/v0.1.2...v0.1.3) (2019-08-27) 65 | 66 | ### Bug Fixes 67 | 68 | - **publish:** fix missing dist folder in v0.1.2 ([7783d6b](https://github.com/pmmmwh/react-polyglot-hooks/commit/7783d6b)) 69 | 70 | ### [0.1.2](https://github.com/pmmmwh/react-polyglot-hooks/compare/v0.1.1...v0.1.2) (2019-08-27) 71 | 72 | ### Features 73 | 74 | - **polyglot:** add T component for easy phrase consumption ([6d6d32c](https://github.com/pmmmwh/react-polyglot-hooks/commit/6d6d32c)) 75 | 76 | ### [0.1.1](https://github.com/pmmmwh/react-polyglot-hooks/compare/v0.1.0...v0.1.1) (2019-08-26) 77 | 78 | ### Bug Fixes 79 | 80 | - **deps-dev:** fix lock file mismatch ([80da34d](https://github.com/pmmmwh/react-polyglot-hooks/commit/80da34d)) 81 | - **polyglot:** make key in NoOp required ([37d68d3](https://github.com/pmmmwh/react-polyglot-hooks/commit/37d68d3)) 82 | - **publish:** properly distribute with the dist folder ([0b58f6f](https://github.com/pmmmwh/react-polyglot-hooks/commit/0b58f6f)) 83 | 84 | ## 0.1.0 (2019-08-26) 85 | 86 | ### Features 87 | 88 | - **polyglot:** add root export for publish ([53b2003](https://github.com/pmmmwh/react-polyglot-hooks/commit/53b2003)) 89 | - **polyglot:** augment polyglot type definitions ([4c5354d](https://github.com/pmmmwh/react-polyglot-hooks/commit/4c5354d)) 90 | - **polyglot:** implement i18n context ([f0f7d5c](https://github.com/pmmmwh/react-polyglot-hooks/commit/f0f7d5c)) 91 | - **polyglot:** implement i18n provider ([8905a53](https://github.com/pmmmwh/react-polyglot-hooks/commit/8905a53)) 92 | - **polyglot:** implement useLocale and useTranslate hooks ([aa8cd11](https://github.com/pmmmwh/react-polyglot-hooks/commit/aa8cd11)) 93 | - **testing:** add test runner script for local and ci ([1f6c1b8](https://github.com/pmmmwh/react-polyglot-hooks/commit/1f6c1b8)) 94 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # React Polyglot Hooks 2 | 3 | Hooks for using [Polyglot.js](https://airbnb.io/polyglot.js) with [React](https://reactjs.org/). 4 | 5 | [![npm Package](https://img.shields.io/npm/v/react-polyglot-hooks/latest.svg)](https://www.npmjs.com/package/react-polyglot-hooks) 6 | [![Minified Size](https://img.shields.io/bundlephobia/min/react-polyglot-hooks)](https://bundlephobia.com/result?p=react-polyglot-hooks@latest) 7 | [![Min-zipped Size](https://img.shields.io/bundlephobia/minzip/react-polyglot-hooks)](https://bundlephobia.com/result?p=react-polyglot-hooks@latest) 8 | 9 | [![CircleCI](https://img.shields.io/circleci/project/github/pmmmwh/react-polyglot-hooks/main.svg)](https://app.circleci.com/pipelines/github/pmmmwh/react-polyglot-hooks?branch=main) 10 | [![Coverage Status](https://img.shields.io/codecov/c/github/pmmmwh/react-polyglot-hooks/main.svg)](https://codecov.io/gh/pmmmwh/react-polyglot-hooks/branch/main) 11 | ![Code Style](https://img.shields.io/badge/code_style-prettier-ff69b4.svg?logo=prettier) 12 | [![Conventional Commits](https://img.shields.io/badge/Conventional%20Commits-1.0.0-yellow.svg)](https://conventionalcommits.org) 13 | [![Dependabot Status](https://api.dependabot.com/badges/status?host=github&repo=pmmmwh/react-polyglot-hooks)](https://dependabot.com) 14 | [![Dependencies](https://david-dm.org/pmmmwh/react-polyglot-hooks/main/status.svg)](https://david-dm.org/pmmmwh/react-polyglot-hooks/main) 15 | [![PeerDependencies](https://david-dm.org/pmmmwh/react-polyglot-hooks/main/peer-status.svg)](https://david-dm.org/pmmmwh/react-polyglot-hooks/main?type=peer) 16 | [![DevDependencies](https://david-dm.org/pmmmwh/react-polyglot-hooks/main/dev-status.svg)](https://david-dm.org/pmmmwh/react-polyglot-hooks/main?type=dev) 17 | 18 | ## Installation 19 | 20 | React Polyglot Hooks is available as an [npm package](https://www.npmjs.com/package/react-polyglot-hooks). 21 | 22 | ```sh 23 | // with npm 24 | npm install react-polyglot-hooks 25 | 26 | // with yarn 27 | yarn add react-polyglot-hooks 28 | ``` 29 | 30 | > React is required as a peer dependency. 31 | > Please install version 16.8.3 or later (minimum requirement for hooks). 32 | 33 | ## Usage 34 | 35 | React Polyglot Hooks offers 1 wrapper component: ``, 2 hooks: `useLocale` and `useT` and 1 helper component: ``. 36 | The hooks and the helper component have to be wrapped with the `` component to work properly. 37 | 38 | Here is a quick example to get you started: 39 | First, wrap a parent component with `` and provide `locale` and `phrases`. 40 | 41 | `Parent.jsx` 42 | 43 | ```jsx 44 | import React from 'react'; 45 | import { I18n } from 'react-polyglot-hooks'; 46 | import Child from './Child'; 47 | 48 | // ... or any ways to determine current locale 49 | const locale = window.locale || 'en'; 50 | 51 | // You can put translations in separate files 52 | const phrases = { 53 | en: { hello: 'Hello, World!' }, 54 | fr: { hello: 'Bonjour, Monde!' }, 55 | }; 56 | 57 | export default function Parent() { 58 | return ( 59 | 60 | 61 | 62 | ); 63 | } 64 | ``` 65 | 66 | Then, in a child component, call the hooks: 67 | 68 | `Child.jsx` 69 | 70 | ```jsx 71 | import React from 'react'; 72 | import { T, useLocale } from 'react-polyglot-hooks'; 73 | 74 | export default function Child() { 75 | const locale = useLocale(); // Current locale: "en" 76 | return ( 77 | 78 | {locale} 79 | 80 | 81 | ); 82 | } 83 | ``` 84 | 85 | That's it! For more in-depth examples, check out the [examples](/examples) directory. 86 | 87 | ### Usage with TypeScript 88 | 89 | Types are baked in as the project is written in [TypeScript](https://www.typescriptlang.org/). 90 | 91 | ## API 92 | 93 | ### `` 94 | 95 | Provides i18n context to the T component and the hooks. Accepts all options supported by [Polyglot.js](https://airbnb.io/polyglot.js). 96 | 97 | #### Props 98 | 99 | | Prop | Type | Required | Description | 100 | | --------------- | ---------------------------------------------------------------------------- | -------- | --------------------------------------------------------------------------------- | 101 | | `children` | `Node` | ✅ | Any node(s) accepted by React. | 102 | | `locale` | `string` | ✅ | Current locale, used for pluralization. | 103 | | `phrases` | `{ [key: string]: string }` | ✅ | Key-value pair of translated phrases, can be nested. | 104 | | `allowMissing` | `boolean` | ❌ | Controls whether missing phrase keys in a `t` call is allowed. | 105 | | `interpolation` | `{ prefix: string, suffix: string }` | ❌ | Controls the prefix and suffix for an interpolation. | 106 | | `onMissingKey` | `(key: string, options: InterpolationOptions, locale: string) => string` | ❌ | A function called when `allowMissing` is `true` and a missing key is encountered. | 107 | | `pluralRules` | `{ pluralTypes: PluralTypes, pluralTypeToLanguages: PluralTypeToLanguages }` | ❌ | Custom pluralization rules to be applied to change language(s) behaviour(s). | 108 | 109 | ### `` 110 | 111 | Renders a phrase according to the props. 112 | 113 | #### Props 114 | 115 | | Props | Type | Required | Description | 116 | | ---------------- | ---------------------- | -------- | --------------------------------------------------- | 117 | | `phrase` | `string` | ✅ | Key of the needed phrase. | 118 | | `count` | `number` | ❌ | A number to be interpolated with `smart_count`. | 119 | | `fallback` | `string` | ❌ | A fallback to be rendered if the phrase is missing. | 120 | | `interpolations` | `InterpolationOptions` | ❌ | See `InterpolationOptions` below. | 121 | 122 | ### `useLocale` 123 | 124 | Returns the current active locale (`string`). 125 | 126 | ### `useT` 127 | 128 | Returns a special function (`t`) which takes in parameters and returns a phrase. 129 | 130 | #### `t(phrase, InterpolationOptions)` 131 | 132 | | Param | Type | Required | Description | 133 | | ---------------------- | ------------------------------------------ | -------- | ---------------------------------------------------------------------------------------------------------- | 134 | | `phrase` | `string` | ✅ | Key of the needed phrase. | 135 | | `InterpolationOptions` | `number` or `{ [key: string]: ReactNode }` | ❌ | A number to be interpolated with `smart_count`, or a key-value pair to interpolate values into the phrase. | 136 | 137 | For more details, please visit the [documentation](https://airbnb.io/polyglot.js) of Polyglot.js. 138 | 139 | ## Changelog 140 | 141 | The changelog is available [here](/CHANGELOG.md). 142 | 143 | ## License 144 | 145 | This project is licensed under the terms of the 146 | [MIT License](/LICENSE). 147 | 148 | ## Acknowledgements 149 | 150 | This project is developed to ease the use of [Polyglot.js](https://airbnb.io/polyglot.js) within [React](https://reactjs.org/), and is highly influenced by [`react-polyglot`](https://github.com/nayaabkhan/react-polyglot). 151 | --------------------------------------------------------------------------------