{"id":24193710,"url":"https://github.com/ftzi/g18n","last_synced_at":"2026-03-06T17:34:34.135Z","repository":{"id":115671294,"uuid":"452115372","full_name":"ftzi/g18n","owner":"ftzi","description":"Alternative internationalization JS/TS solution","archived":false,"fork":false,"pushed_at":"2022-01-26T03:08:09.000Z","size":10,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-05-07T03:37:55.841Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":null,"has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/ftzi.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2022-01-26T02:32:13.000Z","updated_at":"2022-01-26T02:32:13.000Z","dependencies_parsed_at":"2023-12-05T17:00:41.640Z","dependency_job_id":null,"html_url":"https://github.com/ftzi/g18n","commit_stats":null,"previous_names":["hfantauzzi/g18n","brfantauzzi/g18n","ftzi/g18n"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/ftzi/g18n","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ftzi%2Fg18n","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ftzi%2Fg18n/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ftzi%2Fg18n/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ftzi%2Fg18n/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/ftzi","download_url":"https://codeload.github.com/ftzi/g18n/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ftzi%2Fg18n/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":30188160,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-03-06T17:33:53.563Z","status":"ssl_error","status_checked_at":"2026-03-06T17:33:51.678Z","response_time":250,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.5:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2025-01-13T17:45:23.355Z","updated_at":"2026-03-06T17:34:34.098Z","avatar_url":"https://github.com/ftzi.png","language":null,"funding_links":[],"categories":[],"sub_categories":[],"readme":"# g18n\n\n## [This is an early draft on my existing code for a future npm package. It works but it's better not to use it until proper release]\n\n\nI don't like how i18n libs work in JS. While they make sense when you don't want to have all the languages texts loaded at the same time,\nthere are some cases where having all of them is useful, like in React Native Apps.\n\nAlso, creating the dictionaries with the i18n libs may be an issue if you type wrongly some text key or if you forget to translate some texts.\n\nWith this library and with TS, it won't allow missing or wrong texts keys. It also allows having functions for the texts.\n\nInstead of the `language: {textId: translation}` philosophy used by the famous libs, it uses `textId: {language: {translation}` pattern.\n\nIt aims to be easy and fast for the developer to use. Uses proxies to allow its programagical working.\n\n## Usage:\n\n### Translations setup\n```ts\nconst myResource = createResource(['en', 'pt'], {\n  // Simple translations\n  loginRequired: {\n    en: 'You need to be logged to execute this action.',\n    pt: 'Você precisa estar logado para executar esta ação.',\n  },\n  // Groups\n  _license: {\n    pressToBuy: {\n      en: 'Press here to adquire your license!',\n      pt: 'Pressione aqui para adquirir sua licença!',\n    },\n    // Functions. The type of the args are TS enforced.\n    paused: (date: string) =\u003e ({\n      en: `The license is paused until ${date}, as requested!`,\n      pt: `A licença está pausada até ${date}, conforme solicitado!`,\n    }) as const,\n  }\n} as const)\n```\n\n### Usage\n\n```tsx\n// You can use just T when not using React, but it won't automatically rerender on language change.\nexport const { useT, T, language, languages } = createT({\n  resource: myResource,\n  languages: ['en', 'pt'],\n  initialLanguage: 'pt',\n});\n\nexport function Component(): JSX.Element {\n  const { T } = useT();\n  if (!logged)\n    return \u003cText\u003e{T.loginRequired}\u003c/Text\u003e\n  else\n    // TS typesafe argument\n    return \u003cText\u003e{T._license.paused('January 25, 2022')}\u003c/Text\u003e\n}\n```\n\nAs we use `as const`, you may see the translation texts when hovering the T.textId.\n\n\u003cdetails\u003e\u003csummary\u003eCode\u003c/summary\u003e\n\n```ts\nimport { createGlobalState } from 'react-hooks-global-state';\nimport { Id } from '../utils/utils';\n\n\n\nconst defaultLanguage = 'en';\nexport let fallbackLanguage = defaultLanguage;\nlet languages: ReadonlyArray\u003cstring\u003e = [fallbackLanguage];\nlet resource: Resource = {};\nlet language: string = '';\nlet T: any = undefined;\nlet baseNodesProxies: NodeProxy = {};\nlet onLanguageChange: ((language: string) =\u003e void) | undefined = undefined;\n\n/** If calledCreated was already called */\nlet calledCreateT = false;\n/** If {awaitInitT: true} prop was passed in createT */\nlet usingInitT = false;\n/** If createT was already called (and also initT, if configured) */\nlet created = false;\n\n\n\ntype MyGlobalState = {\n  language: string,\n  T: any;\n};\n// From react-hooks-global-state/dist/src/createGlobalState.d.ts\ntype UseGlobalState\u003cState\u003e = \u003cStateKey extends keyof State\u003e(stateKey: StateKey) =\u003e readonly [State[StateKey], (u: import('react').SetStateAction\u003cState[StateKey]\u003e) =\u003e void];\ntype SetGlobalState\u003cState\u003e = \u003cStateKey_2 extends keyof State\u003e(stateKey: StateKey_2, update: import('react').SetStateAction\u003cState[StateKey_2]\u003e) =\u003e void;\nlet useGlobalState: UseGlobalState\u003cMyGlobalState\u003e | undefined = undefined;\nlet setGlobalState: SetGlobalState\u003cMyGlobalState\u003e | undefined = undefined;\n\n\n\n\nexport type Resource\u003cL extends string = string\u003e = {\n  [id: string]: {\n    [language in L]: string;\n  }\n    | ((...args: any[]) =\u003e {[language in L]: string})\n    | Resource\u003cL\u003e\n}\n\n\n\ntype InitTFunParams = {\n  language?: string;\n}\nexport let initT: (args?: InitTFunParams) =\u003e void = () =\u003e {\n  if (!calledCreateT)\n    throw new Error('initT called but createT wasn\\'t called before.');\n  if (!usingInitT)\n    throw new Error('initT called but awaitInitT prop wasn\\'t passed in createT');\n};\n\n\n\ntype I18nParam\u003cL extends string, R extends Resource\u003e = Readonly\u003c{\n  languages?: ReadonlyArray\u003cL\u003e,\n  initialLanguage?: L,\n  resource: R,\n  onLanguageChange?: (language: string) =\u003e void;\n  fallbackLanguage?: string;\n  /** If true, the createT will only take effect when initT() function is called.\n   * @default false */\n  awaitInitT?: boolean;\n  // strictCheck?: boolean // check for extra translations and missing languages on init.\n  // onMissingLanguage?: (textId, language) =\u003e void, function called on missing translation and T used.\n  // showTranslationsTypes // if it will show the translations in intelissense when hovering T.[x].\n\n}\u003e\n\n\nexport function createT\u003cL extends string, R extends Resource\u003e({\n  awaitInitT = false,\n  initialLanguage = 'en' as any,\n  languages: languagesProp = ['en'] as any,\n  resource: resourceProp,\n  onLanguageChange: onLanguageChangeProp,\n  fallbackLanguage: fallbackLanguageProp = defaultLanguage,\n}: I18nParam\u003cL, R\u003e) {\n\n  calledCreateT = true;\n  const fun = (args?: InitTFunParams) =\u003e {\n    // if (created)\n    //   console.warn('initT was called again but createT has been already successfully and fully executed.');\n    fallbackLanguage = fallbackLanguageProp;\n    // Try first to use current language, if Fast Refresh,\n    // then check initT language argument, finally use createT language arg.\n    language = language || args?.language || initialLanguage;\n    resource = resourceProp;\n    onLanguageChange = onLanguageChangeProp;\n    languages = languagesProp;\n    baseNodesProxies = {}; // Reset it.\n    T = createProxy({ resource, subNodesProxies: baseNodesProxies });\n    const createdGlobalState = createGlobalState\u003cMyGlobalState\u003e({ language, T });\n    useGlobalState = createdGlobalState.useGlobalState;\n    setGlobalState = createdGlobalState.setGlobalState;\n    created = true;\n  };\n  if (awaitInitT \u0026\u0026 !created) {\n    // Fast refresh workaround.\n    initT = (args?: InitTFunParams) =\u003e {\n      fun(args);\n      initT = () =\u003e null;\n    };\n    usingInitT = true;\n  } else // If not awaiting or recreating (like Fast Refresh)\n    fun();\n\n  return {\n    useT: () =\u003e useTInternal\u003cL, ParseR\u003cR\u003e\u003e(),\n    // Getters to keep it updated.\n    /** Translator */\n    get T() {\n      return T as unknown as ParseR\u003cR\u003e;\n    },\n    /** Current language */\n    get language() {\n      return language as L;\n    },\n    /** Available languages */\n    get languages() {\n      return languages as L[];\n    },\n  };\n}\n\n\n\nfunction isSubNode(key: string) {\n  return key[0] === '_';\n}\n\n\ntype NodeProxy = Record\u003cstring, any\u003e\n\n\n\n// Outside proxy, so it won't be created for each node.\nconst proxyGet = (\n  { resource, subNodesProxies, selectionId: selectedId }:\n  {resource: Resource\u003cstring\u003e, selectionId: string, subNodesProxies: NodeProxy},\n): any =\u003e {\n\n  if (['$$typeof', 'prototype'].includes(selectedId)) // Those textIds may happen on Fast Refresh in RN, for some unknown reason.\n    return 'TYPE_OF_ERROR'; // it shouldnt appear anywhere, but if it do, we can track it down here.\n\n  // console.log('l, r, t', language, resource, textId);\n\n  const selectedNode = resource[selectedId];\n\n  if (!selectedNode) {\n    const id = selectedId.substr(0, 30);\n    console.warn(`Translations not found. TextId=${id} (name may have been shortened)`);\n    return id;\n  }\n\n\n  if (typeof selectedNode === 'function')\n    return (...args: any) =\u003e selectedNode(...args)[language];\n\n  if (isSubNode(selectedId)) {\n    if (!subNodesProxies[selectedId]) {\n      subNodesProxies[selectedId] = createProxy({ resource: selectedNode as Resource\u003cstring\u003e, subNodesProxies: {} });\n    }\n\n    return subNodesProxies[selectedId];\n  }\n\n  else // Is a simple translation node\n    return selectedNode[language];\n};\n\n\n\nfunction createProxy({ subNodesProxies, resource }: {resource: Resource, subNodesProxies: NodeProxy}) {\n  return new Proxy(resource, {\n    get: (resource, selectionId: string) =\u003e proxyGet({ subNodesProxies, resource, selectionId }),\n  }) as any as Record\u003cstring, string\u003e;\n}\n\n\n\n// TODO overload so languages is optional\n// TODO add some magic so only desired lang is processed in functional translations\n/** @param languages - TS helper */\nexport function createResource\u003cR extends Resource\u003cL\u003e, L extends string\u003e(languages: ReadonlyArray\u003cL\u003e, myResource: R): R {\n  return myResource;\n}\n\n\n\nexport function setLanguage(newLanguage: string) {\n  if (newLanguage !== language) {\n    language = newLanguage;\n    setGlobalState?.('language', newLanguage);\n    T = createProxy({ resource, subNodesProxies: baseNodesProxies }); // We have to recreate the proxy to trigger React dep list. It will only recreate the top level resource proxy (T).\n    setGlobalState?.('T', T);\n    onLanguageChange?.(newLanguage);\n  }\n}\n\n\n\ntype ParseR\u003cR extends Resource\u003e = {[K in keyof R]: R[K] extends (args: any) =\u003e any ? ParseFun\u003cR[K]\u003e : ParseObj\u003cR[K]\u003e}\ntype ParseObj\u003cT extends Record\u003cstring, any\u003e\u003e = T[keyof T] extends string ? Id\u003cT[keyof T]\u003e : Id\u003cParseR\u003cT\u003e\u003e // Union vals if string\ntype ParseFun\u003cT extends (args: any) =\u003e Record\u003cstring, any\u003e\u003e =\n  T extends (...args: infer A) =\u003e infer R ? (...args: A) =\u003e ParseObj\u003cR\u003e : never\n\n// type UseTRtn\u003cL extends string, R extends Resource\u003cL\u003e\u003e = {\n//   language: L,\n//   setLanguage: (language: string) =\u003e void;\n//   T: R\n// }\n/** Hook */\n\n\n\nfunction useTInternal\u003cL extends string, T\u003e() {\n  if (!useGlobalState)\n    throw new Error ('use18 Error: create18 wasn\\'t called!');\n\n  const [internalLanguage] = useGlobalState('language');\n  const [internalT] = useGlobalState('T'); // Using it in a state so T can be used as dep in onEffect etc.\n\n  return {\n    language: internalLanguage as L, // not using internal so dev uses the same language value (on hook and outside)\n    languages: languages as L[],\n    setLanguage: setLanguage,\n    T: internalT as unknown as T,\n  };\n}\n```\n\n\u003c/details\u003e\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fftzi%2Fg18n","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fftzi%2Fg18n","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fftzi%2Fg18n/lists"}