diff --git a/package.json b/package.json index 1a3b161..344dce6 100644 --- a/package.json +++ b/package.json @@ -4,11 +4,15 @@ "private": true, "homepage": "http://profile_generator.volt-bonn.de/", "dependencies": { + "@fluent/bundle": "^0.16.0", + "@fluent/langneg": "^0.5.0", + "@fluent/react": "^0.13.0", "@testing-library/jest-dom": "^5.11.4", "@testing-library/react": "^11.1.0", "@testing-library/user-event": "^12.1.10", "hammerjs": "^2.0.8", "hamsterjs": "^1.1.3", + "intl-pluralrules": "^1.2.2", "merge-images": "^2.0.0", "react": "^17.0.1", "react-dom": "^17.0.1", diff --git a/src/App.js b/src/App.js index b3233ce..0689055 100644 --- a/src/App.js +++ b/src/App.js @@ -9,6 +9,12 @@ import HeaderImage from './HeaderImage.svg' import purpleBG from './purpleBG.png' import empty_1x1 from './empty_1x1.png' +import 'intl-pluralrules' +import { AppLocalizationProvider } from './l10n.js' +import { Localized } from './Localized.js' + +// const userLocales = ['de'] || navigator.languages +const userLocales = navigator.languages const frameSize = 1080 @@ -279,6 +285,7 @@ function App() { return ( +
Volt Logo @@ -286,7 +293,7 @@ function App() { Drop your photo here ...
-

Choose your Photo:

+

It should best be a square image or your face in the middle. The photo is not saved and never leaves your computer.

) } diff --git a/src/Localized.js b/src/Localized.js new file mode 100644 index 0000000..74e9519 --- /dev/null +++ b/src/Localized.js @@ -0,0 +1,55 @@ +import React from 'react' +import { + Localized as LocalizedOriginal, + // withLocalization, +} from '@fluent/react' + +import { FluentContext } from '../node_modules/@fluent/react/esm/context.js' + +const Localized = props => ( + , + ...props.elems, + }} + > + {props.children} + +) + +// A custom withLocalization to have an empty fallback. +// It is nearly identical to the original. +function withLocalization(Inner) { + function WithLocalization(props) { + const l10n = React.useContext(FluentContext) + + const getString = (id, args, fallback) => l10n.getString(id, args, fallback || ' ') + + return React.createElement(Inner, { getString, ...props }) + } + return WithLocalization +} + +export { + withLocalization, + Localized, + Localized as default, +} + +/* + +import { Localized, withLocalization } from '../Localized/' + + +export default withLocalization(componentName) + + +import Localized from '../Localized/' + + +import { withLocalization } from '@fluent/react' +export default withLocalization(componentName) + +*/ diff --git a/src/l10n.js b/src/l10n.js new file mode 100644 index 0000000..bf09342 --- /dev/null +++ b/src/l10n.js @@ -0,0 +1,83 @@ +import React from 'react' + +// https://projectfluent.org/play/ + +// import {LocalizationProvider,Localized} from '@fluent/react' // '@fluent/react/compat' +import { ReactLocalization, LocalizationProvider } from '@fluent/react' +import { FluentBundle, FluentResource } from '@fluent/bundle' +import { negotiateLanguages } from '@fluent/langneg' + +const _supportedLocales_ = [ + 'de', + 'en', +] +const _defaultLocale_ = 'en' + + +async function fetchMessages(locale) { + const path = await import('./locales/' + locale + '.ftl') + + const response = await fetch(path.default) + const messages = await response.text() + + return { [locale]: new FluentResource(messages) } +} + +function getDefaultBundles() { + const bundle = new FluentBundle('') + bundle.addResource(new FluentResource('')) + return new ReactLocalization([bundle]) +} + +async function createMessagesGenerator(currentLocales) { + const fetched = await Promise.all( + currentLocales.map(fetchMessages) + ) + const messages = fetched.reduce( + (obj, cur) => Object.assign(obj, cur) + ) + + return function* generateBundles() { + for (const locale of currentLocales) { + const bundle = new FluentBundle(locale) + bundle.addResource(messages[locale]) + yield bundle + } + } +} + +export class AppLocalizationProvider extends React.Component { + constructor(props) { + super(props) + this.state = { + bundles: getDefaultBundles(), + } + } + + async componentDidMount() { + const currentLocales = negotiateLanguages( + this.props.userLocales, + _supportedLocales_, + { defaultLocale: _defaultLocale_ } + ) + + const generateBundles = await createMessagesGenerator(currentLocales) + this.setState({ bundles: new ReactLocalization(generateBundles()) }) + } + + render() { + const { children } = this.props + const { bundles } = this.state + + if (!bundles) { + // Show a loader. + return
Loading texts…
+ } + + return ( + + {children} + + ) + } +} diff --git a/src/locales/de.ftl b/src/locales/de.ftl new file mode 100644 index 0000000..9f6779a --- /dev/null +++ b/src/locales/de.ftl @@ -0,0 +1 @@ +choose_your_photo = Wähl dein Bild: diff --git a/src/locales/en.ftl b/src/locales/en.ftl new file mode 100644 index 0000000..50aa551 --- /dev/null +++ b/src/locales/en.ftl @@ -0,0 +1 @@ +choose_your_photo = Choose your Photo: diff --git a/yarn.lock b/yarn.lock index b347c0a..72c99f8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1164,6 +1164,30 @@ minimatch "^3.0.4" strip-json-comments "^3.1.1" +"@fluent/bundle@^0.16.0": + version "0.16.0" + resolved "https://registry.yarnpkg.com/@fluent/bundle/-/bundle-0.16.0.tgz#e0cab75ba6ce9d9147ace3cb6ec61fd90f31ec1f" + integrity sha512-kUEAUePhb/y2BCcNpKOnjCs+WJkDczVpUUAQ+cDl0xvBGqL0Kv0Yog2oHSuv/Ou22c6KdXbvfCl3We0bIZnrmg== + +"@fluent/langneg@^0.5.0": + version "0.5.0" + resolved "https://registry.yarnpkg.com/@fluent/langneg/-/langneg-0.5.0.tgz#de448070efa16c8fb6cc4af1629663f01e10689b" + integrity sha512-jv0g3YO5byz29HXEE6DBzAog60q726mwV2nIoekEX590JVh+mbd6/ZXT5/l4mN2BMlrelzyscCTffKI4XScVtg== + +"@fluent/react@^0.13.0": + version "0.13.0" + resolved "https://registry.yarnpkg.com/@fluent/react/-/react-0.13.0.tgz#cf2652d56fe22072dfeb30dcd2e03650fb913f88" + integrity sha512-NdITFI7eqecpb2Ty+I9g2BKxbzhdBKoKm8eU3/vnmBhGDeFgkS/aACSHF3gV2rL87JW9A+h4Ih1ympWp8OqqxQ== + dependencies: + "@fluent/sequence" "0.5.0" + cached-iterable "^0.2.1" + prop-types "^15.6.0" + +"@fluent/sequence@0.5.0": + version "0.5.0" + resolved "https://registry.yarnpkg.com/@fluent/sequence/-/sequence-0.5.0.tgz#6349b614711df2d8ed256598fa31a757fe032539" + integrity sha512-70LhnbPO/sO2rI2vHws66QwKq9a7PKiEN0hrc65xY3LzWq3AlATZnt+EmFG1ihmPI+5mqGPnJMAdpp3qze3X2Q== + "@hapi/address@2.x.x": version "2.1.4" resolved "https://registry.yarnpkg.com/@hapi/address/-/address-2.1.4.tgz#5d67ed43f3fd41a69d4b9ff7b56e7c0d1d0a81e5" @@ -2989,6 +3013,11 @@ cache-base@^1.0.1: union-value "^1.0.0" unset-value "^1.0.0" +cached-iterable@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/cached-iterable/-/cached-iterable-0.2.1.tgz#723958f5e7adc74c96bedb10b426bdfd95f2fe6d" + integrity sha512-8zAVjMjdn/S/QXJaOnqsko0+ZJzXT2Dum2u9TMGg5YR9fxONPrUjuO9VYqnb1AoldXeYVAcNJLgT5Q8WaIJSgA== + call-bind@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.0.tgz#24127054bb3f9bdcb4b1fb82418186072f77b8ce" @@ -5756,6 +5785,11 @@ internal-slot@^1.0.2: has "^1.0.3" side-channel "^1.0.2" +intl-pluralrules@^1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/intl-pluralrules/-/intl-pluralrules-1.2.2.tgz#2b73542a9502a8a3a742cdd917f3d969fb5482fe" + integrity sha512-SBdlNCJAhTA0I0uHg2dn7I+c6BCvSVk6zJ/01ozjwJK7BvKms9RH3w3Sd/Ag24KffZ/Yx6KJRCKAc7eE8TZLNg== + ip-regex@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/ip-regex/-/ip-regex-2.1.0.tgz#fa78bf5d2e6913c911ce9f819ee5146bb6d844e9" @@ -8754,7 +8788,7 @@ prompts@2.4.0, prompts@^2.0.1: kleur "^3.0.3" sisteransi "^1.0.5" -prop-types@^15.7.2: +prop-types@^15.6.0, prop-types@^15.7.2: version "15.7.2" resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5" integrity sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==