Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/i18n #367

Merged
merged 16 commits into from
Jun 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2,454 changes: 1,624 additions & 830 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion results-astro/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
"@devographics/fetch": "workspace:^1.0.0",
"@devographics/helpers": "workspace:^1.0.0",
"@devographics/i18n": "workspace:^1.0.0",
"@devographics/react-i18n": "workspace:^1.0.0",
"@devographics/react-i18n": "workspace:^",
"@devographics/types": "workspace:^1.0.0",
"@radix-ui/react-tabs": "^1.0.1",
"@types/react": "^18.2.75",
Expand All @@ -28,6 +28,7 @@
"zod": "^3.21.4"
},
"devDependencies": {
"@astrojs/react": "^3.0.7",
"@rollup/plugin-yaml": "^4.1.2",
"@types/bun": "^1.1.1",
"@types/react-dom": "^18.3.0",
Expand Down
4 changes: 2 additions & 2 deletions results-astro/src/components/i18n/T.astro
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
---
import { TK_ATTR } from "./attributes";
import { DATA_TOKEN_ATTR } from "@devographics/i18n";
const { i18n } = Astro.locals;
const { token, values, html } = Astro.props;
interface Props {
Expand All @@ -18,6 +18,6 @@ const res = i18n.t(token, values);
// we don't need something as elaborate than client-side, which also needs filtering
---

<span {...{ [TK_ATTR]: token }}
<span {...{ [DATA_TOKEN_ATTR]: token }}
>Astro translation key {html ? res.tHtml : res.t}</span
>
5 changes: 0 additions & 5 deletions results-astro/src/components/i18n/attributes.ts

This file was deleted.

2 changes: 1 addition & 1 deletion results-astro/src/components/layouts/AstroRootLayout.astro
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
---
import { TranslationMode } from "../i18n/TranslationMode";
import { TranslationMode } from "@devographics/react-i18n";
/**
* Main HTML content ~ a root layout in Next
*/
Expand Down
2 changes: 1 addition & 1 deletion results-astro/src/components/layouts/LeftMenuLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import Pagination from "@/components/ui/DummyReact" //'core/pages/Pagination'
import { Sidebar } from "@/components/sidebar/Sidebar"
import Hamburger from '@/components/ui/Hamburger'
import { screenReadersOnly } from '@/lib/theme/mixins'
import { teapot } from '@/lib/i18n/teapot'
import { teapot } from '@devographics/react-i18n'

function useSidebar() {
const [showSidebar, setShowSidebar] = useState<boolean>(false)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { PageContextProvider, type PageContextValue } from './PageContext'
import { KeydownContextProvider } from './KeydownContext'
import LeftMenuLayout, { tokens as LeftMenuLayoutTokens } from './LeftMenuLayout'
import { getTheme } from '@/lib/theme'
import { teapot } from '@/lib/i18n/teapot'
import { teapot } from '@devographics/react-i18n'

// TODO: this is an example of centralizing the tokens used by all children of ReactContextLayout
// In the future this should be built by a bundler plugin automatically
Expand Down
3 changes: 1 addition & 2 deletions results-astro/src/components/sidebar/Nav.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { getPageLabelKey } from "@/lib/sitemap"
//import { getPageLabelKey } from "@/lib/sitemap"
import { usePageContext } from "../layouts/PageContext"
import { T } from "../i18n/T"
import { useI18n } from "@devographics/react-i18n"


Expand Down
10 changes: 5 additions & 5 deletions results-astro/src/lib/i18n/serverContext.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { makeTranslatorFunc, type LocaleWithStrings } from "@devographics/i18n";
import { makeTranslatorFunc, type LocaleParsed } from "@devographics/i18n";

export function astroI18nCtx(localeWithStrings: LocaleWithStrings) {
export function astroI18nCtx(localeDict: LocaleParsed) {
return {
t: makeTranslatorFunc(localeWithStrings),
locale: localeWithStrings,
localizePath: (p: string) => `/${localeWithStrings.id}${p}`,
t: makeTranslatorFunc(localeDict),
locale: localeDict,
localizePath: (p: string) => `/${localeDict.id}${p}`,
}
}
10 changes: 5 additions & 5 deletions results-astro/src/pages/[locale]/[...path].astro
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,14 @@ import { astroEditionWithSitemap } from "@/fetchers/surveyWithSitemap";
import PageTemplate from "@/components/pages/PageTemplate.astro";
import { initTheme } from "@/lib/theme";
// Reason why we use the Bundler moduleResolution: https://stackoverflow.com/questions/58990498/package-json-exports-field-not-working-with-typescript
import { getLocaleWithStrings } from "@devographics/i18n/server";
import { getLocaleDict, filterClientSideStrings } from "@devographics/i18n/server";
import type { PageDefinition } from "@/lib/sitemap";
import {
ReactContextLayout,
tokens as ReactContextLayoutTokens,
} from "@/components/layouts/ReactContextLayout";

import { GlobalClientLayout } from "@/components/layouts/GlobalClientLayout";
import { filterClientSideStrings } from "@/lib/i18n/filterClientSideStrings";
import { astroI18nCtx } from "@/lib/i18n/serverContext";

/**
Expand Down Expand Up @@ -89,18 +88,19 @@ Astro.locals.theme = theme;
const { locale } = Astro.params;
if (!locale) throw new Error("Can't render without locale");
// TODO: we should load locales one at a time during rendering, we don't need all of them at once
const localeWithStrings = await getLocaleWithStrings({
const { locale: localeDict, error} = await getLocaleDict({
localeId: locale,
// TODO: "translationContexts" for a survey are currently hard-coded in the results app code
// and not visible in the "surveys" repo, we need to move them there, or compute the context from the survey id
contexts: ["common", "results", surveyId, surveyId + "_" + edition.year],
});
if (error) throw error
// Translation logic available in all Astro components
Astro.locals.i18n = astroI18nCtx(localeWithStrings);
Astro.locals.i18n = astroI18nCtx(localeDict);
// Filter translations to be sent client-side, for the current page
const tokenExprs = [...ReactContextLayoutTokens];
const clientLocaleWithStrings = filterClientSideStrings(
localeWithStrings,
localeDict,
tokenExprs,
{ surveyId, editionId },
);
Expand Down
2 changes: 1 addition & 1 deletion results/src/core/blocks/other/NewsletterBlock.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import Newsletter from 'core/components/Newsletter'
import { useI18n } from '@devographics/react-i18n'
import { spacing } from 'core/theme'
import NewsletterMC from 'core/components/NewsletterMC'
import NewsletterPOST from 'core/components/NewsletterPOST'
import NewsletterPOST from 'core/components/NewsletterPost'

const NewsletterBlock = () => {
const { translate } = useI18n()
Expand Down
110 changes: 110 additions & 0 deletions results/src/core/components/NewsletterPost.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import React, { useState } from 'react'
import { useI18n } from '@devographics/react-i18n'
import T from 'core/i18n/T'
import { usePageContext } from 'core/helpers/pageContext'
import Button from './Button'
import styled from 'styled-components'
import { fontWeight, spacing } from 'core/theme'
import jsonp from 'jsonp'

export default function NewsletterPOST({ locale }: { locale?: any }) {
const [email, setEmail] = useState('')
const [loading, setLoading] = useState(false)
const [error, setError] = useState<{ message: string } | null>(null)
const [success, setSuccess] = useState<{ message: string } | null>(null)

const postUrl = process.env.GATSBY_EMAIL_POST_URL || ''

const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const email = e.target.value
setEmail(email)
}

const handleSubmit = async (e: SubmitEvent) => {
setLoading(true)

console.log('SUBMITTING')

e.preventDefault()
const result = await fetch(postUrl, {
method: 'POST',
body: JSON.stringify({ 'email_address[email]': email })
})
console.log(result)
}

return (
<div className="newsletter">
<h3 className="newsletter-heading">
<T k="newsletter.stay_tuned" />
</h3>
<p className="newsletter-details">
<T k="newsletter.leave_your_email" />
</p>{' '}
{success ? (
<div className="newsletter-message newsletter-success">{success.message}</div>
) : (
<NewsletterForm
email={email}
loading={loading}
handleSubmit={handleSubmit}
handleChange={handleChange}
/>
)}
{error && (
<Error_ className="newsletter-message newsletter-error">{error.message}</Error_>
)}
</div>
)
}

const NewsletterForm = ({
email,
loading,
handleSubmit,
handleChange
}: //locale
{
email: string
loading?: boolean
handleSubmit?: any
handleChange?: any
}) => {
const { getString } = useI18n()

return (
<div className="newsletter-form">
<Form_ method="post" action={process.env.GATSBY_EMAIL_POST_URL}>
<Input_
className="newsletter-email"
id="email_address_email"
name="email_address[email]"
type="email"
placeholder={getString('newsletter.email')?.t}
onChange={handleChange}
value={email}
disabled={loading}
/>
<Button type="submit" name="subscribe" className="newsletter-button button">
<T k="newsletter.submit" />
</Button>
</Form_>
</div>
)
}

const Input_ = styled.input`
padding: 10px 20px;
`

const Form_ = styled.form`
align-items: center;
display: flex;
gap: ${spacing(0.5)};
`

const Error_ = styled.div`
color: #fe6a6a;
margin-top: ${spacing()};
font-weight: ${fontWeight('bold')};
`
2 changes: 1 addition & 1 deletion results/src/core/helpers/blockHelpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -234,7 +234,7 @@ export const getBlockImage = ({
pageContext: PageContextValue
}) => {
const capturesUrl = `${process.env.GATSBY_ASSETS_URL}/captures/${pageContext.currentEdition.id}`
return `${capturesUrl}${get(pageContext, 'locale.path')}/${block.id}.png`
return `${capturesUrl}/${get(pageContext, 'locale.id')}/${block.id}.png`
}

interface UrlParams {
Expand Down
3 changes: 1 addition & 2 deletions results/src/core/i18n/T.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,7 @@ import { useI18n } from '@devographics/react-i18n'
import { useKeydownContext } from 'core/helpers/keydownContext'

const getGitHubSearchUrl = (k: string, localeId = 'en') =>
`https://github.com/search?q=${k}+repo%3AStateOfJS%2Fstate-of-js-graphql-results-api+path%3A%2Fsrc%2Fi18n%2F${
localeId || 'en'
`https://github.com/search?q=${k}+repo%3AStateOfJS%2Fstate-of-js-graphql-results-api+path%3A%2Fsrc%2Fi18n%2F${localeId || 'en'
}%2F+path%3A%2Fsrc%2Fi18n%2Fen-US%2F&type=Code&ref=advsearch&l=&l=`

interface TProps {
Expand Down
3 changes: 2 additions & 1 deletion shared/fetch/functions/locale.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ import { FetcherFunctionOptions } from '../types'

/**
* Fetch metadata and strings for a specific locale
* @deprecated Use react-i18n version
*
* @deprecated Use @devographics/i18n/server version
*/
export const fetchLocale = async (
options: FetcherFunctionOptions & {
Expand Down
1 change: 1 addition & 0 deletions shared/fetch/functions/locale_converted.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { FetcherFunctionOptions } from '../types'
/**
* Fetch metadata and strings for a specific locale
* with strings in a format expected by surveyform and other apps
* @deprecated Use @devographics/i18n/server
* @returns
*/
export const fetchLocaleConverted = async (
Expand Down
2 changes: 2 additions & 0 deletions shared/fetch/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ export * from './queries'
export * from './refresh'
export * from "./pipeline"
export * from "./pipeline-redis"
export * from "./pipeline-file-logger"
export * from "./pipeline-cached"
export * from "./helpers"
export * from "./types"
export * from "./graphql"
20 changes: 20 additions & 0 deletions shared/fetch/pipeline-cached.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@

import { pipeline } from "./pipeline"
import { fileLoggerPipelineStep } from "./pipeline-file-logger"
import { redisPipelineStep } from "./pipeline-redis"

/**
* Default pipeline
* - Will store the data into a local .json file (used only for debug so far)
* - Will use Redis as the cache
* - Will otherwise call the last step, to be added by the user
* @param param0
* @returns
*/
export function cachedPipeline<TData>({ cacheKey }: { cacheKey: string }) {
return pipeline<TData>().steps(
fileLoggerPipelineStep(cacheKey + ".json"),
redisPipelineStep(cacheKey),
)

}
13 changes: 13 additions & 0 deletions shared/fetch/pipeline-file-logger.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@

import { type FetchPipelineStep } from './pipeline'
import { allowedCachingMethods } from './helpers'
import { logToFile } from '@devographics/debug'

export function fileLoggerPipelineStep(filename: string): FetchPipelineStep<any> {
const allowedCaches = allowedCachingMethods()
return {
name: "logToFile",
set(data) { logToFile(filename, data) },
disabled: !allowedCaches.filesystem,
}
}
4 changes: 3 additions & 1 deletion shared/fetch/pipeline-redis.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@

import { initRedis, fetchJson as fetchRedis, storeRedis } from '@devographics/redis'
import { type FetchPipelineStep } from './pipeline'
import { allowedCachingMethods } from './helpers'

/**
* Generic get/set for redis, for a given cache key
*/
export function redisFetchStep<T = any>(cacheKey: string): FetchPipelineStep<T> {
export function redisPipelineStep<T = any>(cacheKey: string): FetchPipelineStep<T> {
initRedis()
return {
name: "redis",
Expand All @@ -15,5 +16,6 @@ export function redisFetchStep<T = any>(cacheKey: string): FetchPipelineStep<T>
set: async (locales) => {
await storeRedis(cacheKey, locales)
},
disabled: !allowedCachingMethods().redis
}
}
Loading