Skip to content

Commit

Permalink
next-like flags fix
Browse files Browse the repository at this point in the history
  • Loading branch information
eddow committed Jun 8, 2024
1 parent 9576fa7 commit 5aa824d
Show file tree
Hide file tree
Showing 6 changed files with 99 additions and 66 deletions.
2 changes: 0 additions & 2 deletions .github/workflows/node.js.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
25 changes: 23 additions & 2 deletions docs/bonus.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 `<span...` string. This is done transparently client-side

Note, the generic behavior can be set with `setFlagEngine` (taking `'emojis' | 'flag-icons'` ), even though it should be hydrated dynamically
Therefore,

Two `exceptions` lists are kept (one for emojis, one for flag class name): `flagEmojiExceptions` and `flagClassExceptions`. These are for languages who are not bound to a country (by default, it only contains `en` -> `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 = '🏴󠁧󠁢󠁥󠁮󠁧󠁿'
Expand All @@ -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:
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
103 changes: 48 additions & 55 deletions src/tools/flags.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,69 +8,62 @@ import { Locale } from '../types'
export const flagEmojiExceptions: Record<string, string> = { en: '🇬🇧' }
export const flagClassExceptions: Record<string, string> = { en: 'gb' }

const styleSheet = `<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/flag-icon-css/7.2.3/css/flag-icons.min.css" integrity="sha512-bZBu2H0+FGFz/stDN/L0k8J0G8qVsAL0ht1qg5kTwtAheiXwiRKyCq1frwfbSFSJN3jooR5kauE0YjtPzhZtJQ==" crossorigin="anonymous" referrerpolicy="no-referrer" />`
const styleSheet = `\n<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/flag-icon-css/7.2.3/css/flag-icons.min.css" integrity="sha512-bZBu2H0+FGFz/stDN/L0k8J0G8qVsAL0ht1qg5kTwtAheiXwiRKyCq1frwfbSFSJN3jooR5kauE0YjtPzhZtJQ==" crossorigin="anonymous" referrerpolicy="no-referrer" />`

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 `<span class="fi fi-${code}"></span>`
}
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 `<span class="fi fi-${code}"></span>`
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]
}
7 changes: 3 additions & 4 deletions src/umd/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,17 @@ import {
Locale,
Translator,
Translation,
localeFlags,
localeFlagsEngine,
reports,
TContext,
gotUserAgent
TContext
} from '../client'
import { parse } from '../tools/cgpt-js'

declare global {
var T: Translator
}

gotUserAgent(navigator.userAgent)
export const localeFlags = localeFlagsEngine()

Object.assign(reports, {
error(context: TContext, error: string, spec: object) {
Expand Down
26 changes: 24 additions & 2 deletions test/specifics.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,9 @@ import {
bulkDictionary,
bulkObject,
reports,
localeFlags,
localeFlagsEngine,
flagEmojiExceptions,
flagClassExceptions,
parse,
stringify
} from '~/s-a'
Expand Down Expand Up @@ -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(['🇬🇧', '🇺🇸'])
Expand All @@ -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(['<span class="fi fi-gb"></span>'])
expect(localeFlags('en-GB')).toEqual(['<span class="fi fi-gb"></span>'])
expect(localeFlags('en-US-gb')).toEqual([
'<span class="fi fi-gb"></span>',
'<span class="fi fi-us"></span>'
])
flagClassExceptions.en = 'gb-eng'
expect(localeFlags('en-GB')).toEqual([
'<span class="fi fi-gb-eng"></span>',
'<span class="fi fi-gb"></span>'
])
expect(localeFlags('fr')).toEqual(['<span class="fi fi-fr"></span>'])
expect(localeFlags('fr-FR')).toEqual(['<span class="fi fi-fr"></span>'])
expect(localeFlags('fr-BE')).toEqual([
'<span class="fi fi-fr"></span>',
'<span class="fi fi-be"></span>'
])
})
test('errors', async () => {
// TODO test errors
})
Expand Down

0 comments on commit 5aa824d

Please sign in to comment.