From 5aa824df41fe126aac85410ca34200d8eb359ecb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Marie=20De=20Mey?= Date: Sat, 8 Jun 2024 13:27:17 +0300 Subject: [PATCH] next-like flags fix --- .github/workflows/node.js.yml | 2 - docs/bonus.md | 25 ++++++++- package.json | 2 +- src/tools/flags.ts | 103 ++++++++++++++++------------------ src/umd/client.ts | 7 +-- test/specifics.test.ts | 26 ++++++++- 6 files changed, 99 insertions(+), 66 deletions(-) diff --git a/.github/workflows/node.js.yml b/.github/workflows/node.js.yml index f655f18..ecef790 100644 --- a/.github/workflows/node.js.yml +++ b/.github/workflows/node.js.yml @@ -18,8 +18,6 @@ jobs: steps: - uses: actions/checkout@v4 - - name: Disable lock file check - run: echo "::set-output name=check-run::false" - name: Use Node.js ${{ matrix.node-version }} uses: actions/setup-node@v4 with: diff --git a/docs/bonus.md b/docs/bonus.md index c698e39..059bd46 100644 --- a/docs/bonus.md +++ b/docs/bonus.md @@ -7,14 +7,15 @@ The library takes in consideration two cases: - On systems who support flags emojis, flags will just be this, a 2~3 unicode characters that form the emoji - On windows, the library `flag-icons` will be downloaded dynamically from [cdnjs.com](https://cdnjs.com/libraries/flag-icon-css) (~28k) and `localeFlag` will return a ` `gb`) > Note: under windows, you won't see flags here beside '🏴󠁧󠁢󠁥󠁮󠁧󠁿' who is not even the correct one. ```js -import { localeFlags, flagCodeExceptions } +import { localeFlagsEngine, flagEmojiExceptions } +const localeFlags = localeFlagsEngine('emojis') localeFlags('en-GB') // ['🇬🇧'] localeFlags('en-US') //['🇬🇧', '🇺🇸'] flagEmojiExceptions.en = '🏴󠁧󠁢󠁥󠁮󠁧󠁿' @@ -24,6 +25,26 @@ localeFlags('en-GB') // ['🏴󠁧󠁢󠁥󠁮󠁧󠁿', '🇬🇧'] > Note: The returned strings must therefore be considered as html code, not pure text, even if for most, it will be pure text +`localeFlagsEngine` can be called either with an engine name (`emojis`/`flag-icons`) either with a userAgent (from the request header) either with nothing if called from the client. + +`localeFlagsEngine` return a scpecific type (`LocaleFlagsEngine`) who has a property `headerContent` who perhaps contain a style node (html) to add to the header + +### For client-only + +The UMD client export a `localFlags` function, everything is automated (even adding the stylesheet reference if needed) + +### For served content + +The `localeFlagsEngine` function can be called with the `user-agent` request header. + +In order to retrieve the engine name, when transferring data to "client" (SSR/browser), `localeFlags.name` can be used. + +### But ... why ? + +Why asking the server to tell the client if it runs on windows ? It's indeed the only way to solve two somehow contradictory issues : +- Make sure no extra download is done. Each Kb file to be downloaded is latency on mobile app +- Make sure there is no "blinking" on load (when the generated page differs from the `onMount` result), even on windows machines + ## js-like "jsonability" The dictionary uses a human "json" format. It's really minimalistic and didn't deserve the 25k of `json5` or `hjson`, it doesn't have more ability than json but: diff --git a/package.json b/package.json index 7edc949..ed279c5 100644 --- a/package.json +++ b/package.json @@ -64,7 +64,7 @@ "jest": "^29.7.0", "jsdoc-to-markdown": "^8.0.1", "prettier": "^3.2.5", - "rollup": "^4.16.4", + "rollup": "^4.18.0", "rollup-plugin-dts": "^6.1.0", "rollup-plugin-typescript2": "^0.36.0", "ts-jest": "^29.1.2", diff --git a/src/tools/flags.ts b/src/tools/flags.ts index 9fe11a9..a733660 100644 --- a/src/tools/flags.ts +++ b/src/tools/flags.ts @@ -8,69 +8,62 @@ import { Locale } from '../types' export const flagEmojiExceptions: Record = { en: '🇬🇧' } export const flagClassExceptions: Record = { en: 'gb' } -const styleSheet = `` +const styleSheet = `\n` export let flagEngine: 'emojis' | 'flag-icons' export let headStyle: string = '' -flagEngine = 'emojis' // Do not initialize on declaration, rollup considers it as a constant -export function setFlagEngine(engine: 'emojis' | 'flag-icons') { - flagEngine = engine - headStyle = engine === 'flag-icons' ? styleSheet : '' - if (engine === 'flag-icons' && typeof window !== undefined) { - let alreadyHasStyleSheet = false - //const stylesheets = document.querySelectorAll('link[rel="stylesheet"][href*="/flag-icons."]') - const stylesheets = document.querySelectorAll('link[rel="stylesheet"]') - for (let i = 0; i < stylesheets.length; i++) { - if (stylesheets[i].getAttribute('href')?.includes('/flag-icons.')) { - alreadyHasStyleSheet = true - break - } - } - if (!alreadyHasStyleSheet) document.head.insertAdjacentHTML('beforeend', styleSheet) - } +export interface LocaleFlagsEngine { + (locale: Locale): string[] + headerContent?: string } -/** - * Set the global flag engine based on the user agent - * @param userAgent The kind of string returned by `navigator.userAgent` or given in the `user-agent` request header - */ -export function gotUserAgent(userAgent?: string) { - let dftUA: string | undefined - if (typeof navigator !== 'undefined') dftUA = navigator.userAgent - if ((dftUA ?? userAgent)?.toLowerCase()?.includes('windows')) setFlagEngine('flag-icons') +const engines: Record<'emojis' | 'flag-icons', LocaleFlagsEngine> = { + emojis(locale: Locale) { + const parts = locale + .toLowerCase() + .split('-', 3) + .slice(0, 2) + .map( + (code) => + flagEmojiExceptions[code] || + String.fromCodePoint(...Array.from(code).map((k) => k.charCodeAt(0) + 127365)) + ) + return parts[0] === parts[1] ? [parts[0]] : parts + }, + 'flag-icons'(locale: Locale) { + function createSpan(code: string) { + return `` + } + const parts = locale + .toLowerCase() + .split('-', 2) + .map((code) => createSpan(flagClassExceptions[code] || code)) + return parts[0] === parts[1] ? [parts[0]] : parts + } } +engines['flag-icons'].headerContent = styleSheet +engines.emojis.headerContent = '' -function localeFlagsEmojis(locale: Locale) { - const parts = locale - .toLowerCase() - .split('-', 3) - .slice(0, 2) - .map( - (code) => - flagEmojiExceptions[code] || - String.fromCodePoint(...Array.from(code).map((k) => k.charCodeAt(0) + 127365)) +export function localeFlagsEngine( + agent?: string | 'emojis' | 'flag-icons' | null +): LocaleFlagsEngine { + let engineName: 'emojis' | 'flag-icons' + if (agent && ['emojis', 'flag-icons'].includes(agent)) + engineName = agent as 'emojis' | 'flag-icons' + else { + if (!agent && typeof navigator !== 'undefined') agent = navigator.userAgent + engineName = agent + ? agent.toLowerCase().includes('windows') + ? 'flag-icons' + : 'emojis' + : 'emojis' // Server-side default decision + } + if (engineName === 'flag-icons' && typeof document !== 'undefined') { + const flagIconsStylesheets = document.querySelectorAll( + 'link[rel="stylesheet"][href*="/flag-icons."]' ) - return parts[0] === parts[1] ? [parts[0]] : parts -} - -function localeFlagsIcons(locale: Locale) { - function createSpan(code: string) { - return `` + if (!flagIconsStylesheets.length) document.head.insertAdjacentHTML('beforeend', styleSheet) } - const parts = locale - .toLowerCase() - .split('-', 2) - .map((code) => createSpan(flagClassExceptions[code] || code)) - return parts[0] === parts[1] ? [parts[0]] : parts -} - -/** - * Gets one or two html strings representing the flags for the given locale (2 in case of `en-US` for example) - * @param locale The locale - * @param engine Optional: specify wether the targeted system is windows or not (if not, just use emojis) - * @returns - */ -export function localeFlags(locale: Locale, engine?: 'emojis' | 'flag-icons'): string[] { - return (engine ?? flagEngine) === 'emojis' ? localeFlagsEmojis(locale) : localeFlagsIcons(locale) + return engines[engineName] } diff --git a/src/umd/client.ts b/src/umd/client.ts index 0894a3d..cb7beb6 100644 --- a/src/umd/client.ts +++ b/src/umd/client.ts @@ -5,10 +5,9 @@ import { Locale, Translator, Translation, - localeFlags, + localeFlagsEngine, reports, - TContext, - gotUserAgent + TContext } from '../client' import { parse } from '../tools/cgpt-js' @@ -16,7 +15,7 @@ declare global { var T: Translator } -gotUserAgent(navigator.userAgent) +export const localeFlags = localeFlagsEngine() Object.assign(reports, { error(context: TContext, error: string, spec: object) { diff --git a/test/specifics.test.ts b/test/specifics.test.ts index 1e0582b..239b3f5 100644 --- a/test/specifics.test.ts +++ b/test/specifics.test.ts @@ -9,8 +9,9 @@ import { bulkDictionary, bulkObject, reports, - localeFlags, + localeFlagsEngine, flagEmojiExceptions, + flagClassExceptions, parse, stringify } from '~/s-a' @@ -87,7 +88,8 @@ describe('specifics', () => { const T = await CSC.enter() expect(T.test.only()).toBe('only') }) - test('flags', async () => { + test('emojis flags', async () => { + const localeFlags = localeFlagsEngine('emojis') expect(localeFlags('en')).toEqual(['🇬🇧']) expect(localeFlags('en-GB')).toEqual(['🇬🇧']) expect(localeFlags('en-US-gb')).toEqual(['🇬🇧', '🇺🇸']) @@ -97,6 +99,26 @@ describe('specifics', () => { expect(localeFlags('fr-FR')).toEqual(['🇫🇷']) expect(localeFlags('fr-BE')).toEqual(['🇫🇷', '🇧🇪']) }) + test('flag-icons flags', async () => { + const localeFlags = localeFlagsEngine('flag-icons') + expect(localeFlags('en')).toEqual(['']) + expect(localeFlags('en-GB')).toEqual(['']) + expect(localeFlags('en-US-gb')).toEqual([ + '', + '' + ]) + flagClassExceptions.en = 'gb-eng' + expect(localeFlags('en-GB')).toEqual([ + '', + '' + ]) + expect(localeFlags('fr')).toEqual(['']) + expect(localeFlags('fr-FR')).toEqual(['']) + expect(localeFlags('fr-BE')).toEqual([ + '', + '' + ]) + }) test('errors', async () => { // TODO test errors })