Skip to content

Commit

Permalink
Perf: remove Qwik City dependency
Browse files Browse the repository at this point in the history
  • Loading branch information
robisim74 committed Sep 14, 2022
1 parent 9d61fda commit 0e1f6e3
Show file tree
Hide file tree
Showing 13 changed files with 116 additions and 127 deletions.
101 changes: 48 additions & 53 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,21 +103,59 @@ Assets will be loaded through the implementation of `getTranslation$` function b
}
}
```
Add the `QwikSpeak` component in `root.tsx` as a child component of `QwikCity`:
### Custom APIs
```typescript
import { $ } from '@builder.io/qwik';

export const getTranslation$: GetTranslationFn = $((lang: string, asset: string, url?: URL) => {
/* Must contain the logic to get translation data */

// E.g. Fetch translation data from json files in public dir or i18n/[lang]/[asset].json endpoint
let endpoint = '';
// Absolute urls on server
if (isServer && url) {
endpoint = url.origin;
}
endpoint += `/i18n/${lang}/${asset}.json`;
const data = await fetch(endpoint);
return data.json();
});

export const resolveLocale$: ResolveLocaleFn = $((url?: URL) => {
/* Must contain the logic to resolve which locale to use during SSR */
});

export const storeLocale$: StoreLocaleFn = $((locale: SpeakLocale, url?: URL) => {
/* Must contain the logic to store the locale on Client when changes */
});

export const handleMissingTranslation$: HandleMissingTranslationFn = $((key: string, value?: string, params?: any, ctx?: SpeakState) => {
/* Must contain the logic to handle missing values: by default returns the key */
});

export const translateFn: TranslateFn = {
getTranslation$: getTranslation$,
/* other functions */
};
```

Add the `QwikSpeak` component in `root.tsx`:
```jsx
import { QwikSpeak } from 'qwik-speak';

export default component$(() => {
return (
<QwikCity>
{/* Init Qwik Speak (only available in child components) */}
<QwikSpeak config={config}>
/**
* Init Qwik Speak (only available in child components)
*/
<QwikSpeak config={config} translateFn={translateFn}>
<QwikCity>
<Head />
<body>
<RouterOutlet />
</body>
</QwikSpeak>
</QwikCity>
</QwikCity>
</QwikSpeak>
);
});
```
Expand Down Expand Up @@ -149,48 +187,7 @@ export default component$(() => {
);
});
```
The translation data of the additional languages are preloaded along with the current language. They can be used as a fallback for missing values by implementing `handleMissingTranslation$` below, or for multilingual pages
### Hacking the library
```typescript
import { $ } from '@builder.io/qwik';

export const getTranslation$: GetTranslationFn = $((lang: string, asset: string, location?: RouteLocation) => {
/* Must contain the logic to get translation data */
});

export const resolveLocale$: ResolveLocaleFn = $((location?: RouteLocation, endpointData?: any) => {
/* Must contain the logic to resolve which locale to use during SSR */
});

export const storeLocale$: StoreLocaleFn = $((locale: SpeakLocale) => {
/* Must contain the logic to store the locale on Client when changes */
});

export const handleMissingTranslation$: HandleMissingTranslationFn = $((key: string, value?: string, params?: any, ctx?: SpeakState) => {
/* Must contain the logic to handle missing values: by default returns the key */
});

export const translateFn: TranslateFn = {
getTranslation$: getTranslation$,
/* other functions */
};
```
```jsx
export default component$(() => {
return (
<QwikCity>
{/* Init Qwik Speak with translation functions */}
<QwikSpeak config={config} translateFn={translateFn}>
<Head />
<body>
<RouterOutlet />
</body>
</QwikSpeak>
</QwikCity>
);
});
```
Examples of these implementations can be found in the [sample app](https://github.com/robisim74/qwik-speak/tree/main/src/app)
The translation data of the additional languages are preloaded along with the current language. They can be used as a fallback for missing values by implementing `handleMissingTranslation$` below, or for multilingual pages.

## Production
### Using a server
Expand Down Expand Up @@ -255,9 +252,8 @@ Formats a date
- `formatNumber(value: number | string, options?: Intl.NumberFormatOptions, locale?: SpeakLocale, lang?: string, currency?: string)`
Formats a number

- `changeLocale(newLocale: SpeakLocale, ctx: SpeakState, location?: RouteLocation)`
- `changeLocale(newLocale: SpeakLocale, ctx: SpeakState, url?: URL)`
Changes locale at runtime: loads translation data and rerenders components that uses translations

### Speak context
- `useSpeakContext()`
Returns the Speak context
Expand Down Expand Up @@ -315,10 +311,9 @@ dist
Translations are also serialized and made available at runtime.

## What's new
> Released v0.0.11
> Released v0.0.12
- Add plural function
- Add SSG sample
- Removed QwikCity dependency from the library

## License
MIT
3 changes: 1 addition & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,7 @@
"typecheck": "tsc --noEmit"
},
"peerDependencies": {
"@builder.io/qwik": ">=0.0.108",
"@builder.io/qwik-city": ">=0.0.108"
"@builder.io/qwik": ">=0.0.108"
},
"devDependencies": {
"@builder.io/qwik": "0.0.108",
Expand Down
7 changes: 3 additions & 4 deletions src/app/components/header/change-locale.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,19 @@
import { component$ } from '@builder.io/qwik';
import { useLocation } from '@builder.io/qwik-city';

import { translate as t } from '../../../library/translate';
import { useSpeakContext } from '../../../library/use-functions';
import { useSpeakContext, useUrl } from '../../../library/use-functions';
import { changeLocale } from '../../../library/change-locale';

export const ChangeLocale = component$(() => {
const ctx = useSpeakContext();
const location = useLocation();
const url = useUrl();

return (
<div class="change-locale">
<span>{t('app.changeLocale')}</span>
{ctx.config.supportedLocales.map(locale => (
<div class={{ active: locale.lang == ctx.locale.lang, button: true }}
onClick$={async () => await changeLocale(locale, ctx, location)}>
onClick$={async () => await changeLocale(locale, ctx, url)}>
{locale.lang}
</div>
))}
Expand Down
59 changes: 29 additions & 30 deletions src/app/speak-config.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { $ } from '@builder.io/qwik';
import { isServer } from '@builder.io/qwik/build';
import { RouteLocation } from '@builder.io/qwik-city';
import { SpeakConfig, SpeakLocale, SpeakState, TranslateFn } from '../library/types';
import { GetTranslationFn, ResolveLocaleFn, StoreLocaleFn, HandleMissingTranslationFn } from '../library/types';
import { getValue } from '../library/core';
Expand All @@ -20,47 +19,47 @@ export const config: SpeakConfig = {
};

// E.g. Fetch translation data from json files in public dir or i18n/[lang]/[asset].json endpoint
export const getTranslation$: GetTranslationFn = $(async (
lang: string,
asset: string,
location?: RouteLocation
) => {
let url = '';
export const getTranslation$: GetTranslationFn = $(async (lang: string, asset: string, url?: URL) => {
let endpoint = '';
// Absolute urls on server
if (isServer && location?.href) {
url = new URL(location.href).origin;
if (isServer && url) {
endpoint = url.origin;
}
url += `/i18n/${lang}/${asset}.json`;
const data = await fetch(url);
endpoint += `/i18n/${lang}/${asset}.json`;
const data = await fetch(endpoint);
return data.json();
});

// E.g. Resolve locale by url during SSR
export const resolveLocale$: ResolveLocaleFn = $((location?: RouteLocation) => {
const pathLang = location?.params?.lang;
const lang = pathLang || config.defaultLocale.lang;
const locale = config.supportedLocales.find(x => x.lang == lang);
return locale;
export const resolveLocale$: ResolveLocaleFn = $((url?: URL) => {
if (url) {
const pathLang = config.supportedLocales.find(x => url.pathname.startsWith(`/${x.lang}`))?.lang;
const lang = pathLang || config.defaultLocale.lang;
const locale = config.supportedLocales.find(x => x.lang == lang);
return locale;
}
return null;
});

// E.g. Store locale on Client replacing url
export const storeLocale$: StoreLocaleFn = $((locale: SpeakLocale) => {
const url = new URL(window.location.href);
const lang = config.supportedLocales.find(x => url.pathname.startsWith(`/${x.lang}`))?.lang;
export const storeLocale$: StoreLocaleFn = $((locale: SpeakLocale, url?: URL) => {
if (url) {
const pathLang = config.supportedLocales.find(x => url.pathname.startsWith(`/${x.lang}`))?.lang;

const regex = new RegExp(`(/${lang}/)|(/${lang}$)`);
const segment = url.pathname.match(regex)?.[0];
const regex = new RegExp(`(/${pathLang}/)|(/${pathLang}$)`);
const segment = url.pathname.match(regex)?.[0];

if (lang && segment) {
let newSegment = '';
if (locale.lang !== config.defaultLocale.lang) {
newSegment = segment.replace(lang, locale.lang);
} else {
newSegment = '/';
if (pathLang && segment) {
let newSegment = '';
if (locale.lang !== config.defaultLocale.lang) {
newSegment = segment.replace(pathLang, locale.lang);
} else {
newSegment = '/';
}
url.pathname = url.pathname.replace(segment, newSegment);
} else if (locale.lang !== config.defaultLocale.lang) {
url.pathname = `/${locale.lang}${url.pathname}`;
}
url.pathname = url.pathname.replace(segment, newSegment);
} else if (locale.lang !== config.defaultLocale.lang) {
url.pathname = `/${locale.lang}${url.pathname}`;
}

window.history.pushState({}, '', url);
Expand Down
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export {
useSpeakLocale,
useTranslation,
useSpeakConfig,
useUrl,
} from './library/use-functions';
// Components
export { QwikSpeak } from './library/qwik-speak';
Expand Down
10 changes: 4 additions & 6 deletions src/library/change-locale.ts
Original file line number Diff line number Diff line change
@@ -1,28 +1,26 @@
import { RouteLocation } from '@builder.io/qwik-city';

import type { SpeakLocale, SpeakState } from './types';
import { loadTranslation } from './core';

/**
* Change locale at runtime: loads translation data and rerenders components that uses translations
* @param newLocale The new locale to set
* @param ctx Speak context
* @param location Optional Qwik City location context
* @param url Optional URL object
*/
export const changeLocale = async (
newLocale: SpeakLocale,
ctx: SpeakState,
location?: RouteLocation
url?: URL
): Promise<void> => {
const { locale, translation, translateFn } = ctx;

// Load translation data
const loadedTranslation = await loadTranslation(newLocale.lang, ctx, location);
const loadedTranslation = await loadTranslation(newLocale.lang, ctx, url);

// Update state
Object.assign(translation, loadedTranslation);
Object.assign(locale, newLocale);

// Store the locale
await translateFn.storeLocale$(newLocale);
await translateFn.storeLocale$(newLocale, url);
};
2 changes: 1 addition & 1 deletion src/library/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@ import { createContext } from '@builder.io/qwik';

import type { SpeakState } from './types';

export const SpeakContext = createContext<SpeakState>('qwik.speak.state');
export const SpeakContext = createContext<SpeakState>('qwikspeak');
8 changes: 3 additions & 5 deletions src/library/core.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import { RouteLocation } from '@builder.io/qwik-city';

import type { Translation, SpeakState } from './types';

/**
Expand All @@ -8,14 +6,14 @@ import type { Translation, SpeakState } from './types';
export const loadTranslation = async (
lang: string,
ctx: SpeakState,
location?: RouteLocation,
url?: URL,
assets?: string[]
): Promise<Translation> => {
const { config, translateFn } = ctx;

assets = assets ?? config.assets;
// Get translation
const tasks = assets.map(asset => translateFn.getTranslation$(lang, asset, location));
const tasks = assets.map(asset => translateFn.getTranslation$(lang, asset, url));
const results = await Promise.all(tasks);

const translation: Translation = {};
Expand All @@ -29,7 +27,7 @@ export const loadTranslation = async (

export const addData = (translation: Translation, data: Translation, lang: string): void => {
translation[lang] = translation[lang] !== undefined
? { ...translation[lang], ...data }
? { ...translation[lang], ...data } // Shallow merge
: data;
};

Expand Down
14 changes: 5 additions & 9 deletions src/library/qwik-speak.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { component$, Slot, useContextProvider, useMount$, useStore } from '@builder.io/qwik';
import { useEndpoint, useLocation } from '@builder.io/qwik-city';
import { isServer } from '@builder.io/qwik/build';

import type { InternalSpeakState, SpeakConfig, SpeakState, TranslateFn } from './types';
import { getTranslation$, resolveLocale$, setLocale$, handleMissingTranslation$ } from './constants';
import { SpeakContext } from './context';
import { loadTranslation } from './core';
import { useUrl } from './use-functions';

export interface QwikSpeakProps {
/**
Expand Down Expand Up @@ -48,17 +48,13 @@ export const QwikSpeak = component$((props: QwikSpeakProps) => {

useContextProvider(SpeakContext, ctx);

// Get location data
const location = useLocation();
// Get endopoint data
const resource = useEndpoint<any>();
// Get URL object
const url = useUrl();

// Will block the rendering until callback resolves
useMount$(async () => {
const endpointData = await resource.promise;

// Resolve the locale
let resolvedLocale = await translateFn.resolveLocale$(location, endpointData);
let resolvedLocale = await translateFn.resolveLocale$(url);

if (!resolvedLocale) {
resolvedLocale = config.defaultLocale;
Expand All @@ -69,7 +65,7 @@ export const QwikSpeak = component$((props: QwikSpeakProps) => {

// Load translation data
for (const lang of resolvedLangs) {
const loadedTranslation = await loadTranslation(lang, ctx, location);
const loadedTranslation = await loadTranslation(lang, ctx, url);
Object.assign(translation, loadedTranslation);
}

Expand Down
Loading

0 comments on commit 0e1f6e3

Please sign in to comment.