├── resources ├── iso-639.csv.gz ├── iso-639.sql.gz └── wikipedia.csv ├── .gitignore ├── phpunit.xml ├── composer.json ├── .github └── workflows │ ├── test.yml │ ├── lint.yml │ └── release.yml ├── .php-cs-fixer.php ├── README.md ├── src └── ISO639.php └── tests └── ISO639Test.php /resources/iso-639.csv.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matriphe/php-iso-639/HEAD/resources/iso-639.csv.gz -------------------------------------------------------------------------------- /resources/iso-639.sql.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matriphe/php-iso-639/HEAD/resources/iso-639.sql.gz -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor 2 | composer.lock 3 | *.cache 4 | 5 | # PhpStorm IDE 6 | /.idea 7 | 8 | # Coverage reports 9 | /coverage 10 | coverage.xml 11 | .phpunit.cache -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | tests 8 | 9 | 10 | 11 | 12 | src 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "matriphe/iso-639", 3 | "description": "PHP library to convert ISO-639-1 code to language name.", 4 | "keywords": ["iso", "iso-639", "639", "lang", "language", "laravel"], 5 | "type": "library", 6 | "require-dev": { 7 | "phpunit/phpunit": ">=10", 8 | "friendsofphp/php-cs-fixer": "^3.75" 9 | }, 10 | "license": "MIT", 11 | "authors": [ 12 | { 13 | "name": "Muhammad Zamroni", 14 | "email": "halo@matriphe.com" 15 | } 16 | ], 17 | "require": { 18 | "php": "8.*" 19 | }, 20 | "suggest": { 21 | "ext-mbstring": "For better multibyte character support in text transformations", 22 | "ext-xdebug": "For code coverage when running tests" 23 | }, 24 | "autoload": { 25 | "psr-4": { 26 | "Matriphe\\ISO639\\": "src/" 27 | } 28 | }, 29 | "scripts": { 30 | "lint": "php-cs-fixer fix --dry-run --diff", 31 | "lint:fix": "php-cs-fixer fix", 32 | "test": "phpunit", 33 | "test:coverage": "XDEBUG_MODE=coverage phpunit --coverage-text", 34 | "test:coverage-html": "XDEBUG_MODE=coverage phpunit --coverage-html coverage", 35 | "test:coverage-xml": "XDEBUG_MODE=coverage phpunit --coverage-clover coverage.xml", 36 | "test:no-coverage": "phpunit --no-coverage", 37 | "check": [ 38 | "@lint", 39 | "@test" 40 | ] 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Run Test 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | branches: 9 | - master 10 | 11 | permissions: 12 | contents: read 13 | 14 | jobs: 15 | run-test: 16 | strategy: 17 | fail-fast: false 18 | matrix: 19 | operating-system: [ ubuntu-latest ] 20 | php-version: 21 | - "8.4" 22 | - "8.3" 23 | - "8.2" 24 | - "8.1" 25 | 26 | name: PHP ${{ matrix.php-version }} 27 | runs-on: ${{ matrix.operating-system }} 28 | 29 | steps: 30 | - name: Checkout code 31 | uses: actions/checkout@v5 32 | 33 | - name: Setup PHP 34 | uses: shivammathur/setup-php@v2 35 | with: 36 | php-version: ${{ matrix.php-version }} 37 | extensions: mbstring xdebug 38 | tools: composer:v2 39 | 40 | - name: Check PHP extensions 41 | run: php -m 42 | 43 | - name: Validate composer.json and composer.lock 44 | run: composer validate --strict 45 | 46 | - name: Cache Composer packages 47 | id: composer-cache 48 | uses: actions/cache@v3 49 | with: 50 | path: vendor 51 | key: ${{ runner.os }}-php-${{ matrix.php-version }}-${{ hashFiles('**/composer.lock') }} 52 | restore-keys: | 53 | ${{ runner.os }}-php-${{ matrix.php-version }} 54 | 55 | - name: Install dependencies 56 | run: composer install --prefer-dist --no-progress 57 | 58 | - name: Run test 59 | run: composer run-script test:coverage 60 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: Code Linting 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | lint: 10 | name: Lint 11 | runs-on: ubuntu-latest 12 | 13 | permissions: 14 | contents: read 15 | packages: read 16 | # To report GitHub Actions status checks 17 | statuses: write 18 | 19 | steps: 20 | - name: Checkout code 21 | uses: actions/checkout@v5 22 | with: 23 | # super-linter needs the full git history to get the 24 | # list of files that changed across commits 25 | fetch-depth: 0 26 | persist-credentials: false 27 | 28 | - name: Setup PHP 29 | uses: shivammathur/setup-php@v2 30 | with: 31 | php-version: 8.4 32 | extensions: mbstring 33 | tools: composer:v2 34 | 35 | - name: Cache Composer packages 36 | id: composer-cache 37 | uses: actions/cache@v3 38 | with: 39 | path: vendor 40 | key: ${{ runner.os }}-php-8.4-${{ hashFiles('**/composer.lock') }} 41 | restore-keys: | 42 | ${{ runner.os }}-php-8.4 43 | 44 | - name: Install dependencies 45 | run: composer install --prefer-dist --no-progress 46 | 47 | - name: Run PHP-CS-Fixer 48 | run: composer run-script lint 49 | 50 | - name: Check for changes 51 | run: | 52 | if [ -n "$(git status --porcelain)" ]; then 53 | echo "Code style issues found. Please run 'composer run-script fix' locally." 54 | git diff 55 | exit 1 56 | else 57 | echo "No code style issues found." 58 | fi -------------------------------------------------------------------------------- /.php-cs-fixer.php: -------------------------------------------------------------------------------- 1 | in([ 5 | __DIR__ . '/src', 6 | __DIR__ . '/tests', 7 | ]) 8 | ->exclude('vendor'); 9 | 10 | return (new PhpCsFixer\Config()) 11 | ->setRiskyAllowed(true) 12 | ->setRules([ 13 | '@PSR12' => true, 14 | 'array_syntax' => ['syntax' => 'short'], 15 | 'binary_operator_spaces' => true, 16 | 'blank_line_after_opening_tag' => true, 17 | 'cast_spaces' => true, 18 | 'concat_space' => ['spacing' => 'one'], 19 | 'function_typehint_space' => true, 20 | 'lowercase_cast' => true, 21 | 'method_argument_space' => true, 22 | 'native_function_casing' => true, 23 | 'new_with_braces' => true, 24 | 'no_blank_lines_after_class_opening' => true, 25 | 'no_blank_lines_after_phpdoc' => true, 26 | 'no_closing_tag' => true, 27 | 'no_empty_phpdoc' => true, 28 | 'no_empty_statement' => true, 29 | 'no_extra_blank_lines' => [ 30 | 'tokens' => [ 31 | 'extra', 32 | 'throw', 33 | 'use', 34 | ] 35 | ], 36 | 'no_leading_import_slash' => true, 37 | 'no_leading_namespace_whitespace' => true, 38 | 'no_mixed_echo_print' => true, 39 | 'no_multiline_whitespace_around_double_arrow' => true, 40 | 'no_short_bool_cast' => true, 41 | 'no_singleline_whitespace_before_semicolons' => true, 42 | 'no_spaces_after_function_name' => true, 43 | 'no_spaces_around_offset' => true, 44 | 'no_spaces_inside_parenthesis' => true, 45 | 'no_trailing_comma_in_list_call' => true, 46 | 'no_trailing_whitespace' => true, 47 | 'no_trailing_whitespace_in_comment' => true, 48 | 'no_unused_imports' => true, 49 | 'no_whitespace_before_comma_in_array' => true, 50 | 'no_whitespace_in_blank_line' => true, 51 | 'normalize_index_brace' => true, 52 | 'object_operator_without_whitespace' => true, 53 | 'ordered_imports' => true, 54 | 'return_type_declaration' => true, 55 | 'short_scalar_cast' => true, 56 | 'single_blank_line_at_eof' => true, 57 | 'single_class_element_per_statement' => true, 58 | 'single_import_per_statement' => true, 59 | 'single_line_after_imports' => true, 60 | 'single_quote' => true, 61 | 'space_after_semicolon' => true, 62 | 'standardize_not_equals' => true, 63 | 'ternary_operator_spaces' => true, 64 | 'trailing_comma_in_multiline' => ['elements' => ['arrays']], 65 | 'trim_array_spaces' => true, 66 | 'unary_operator_spaces' => true, 67 | 'visibility_required' => true, 68 | 'whitespace_after_comma_in_array' => true, 69 | ]) 70 | ->setFinder($finder); -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PHP ISO-639 2 | 3 | [![Run Test](https://github.com/matriphe/php-iso-639/actions/workflows/test.yml/badge.svg)](https://github.com/matriphe/php-iso-639/actions/workflows/test.yml) 4 | [![Total Download](https://img.shields.io/packagist/dt/matriphe/iso-639.svg)](https://packagist.org/packages/matriphe/iso-639) 5 | [![Latest Stable Version](https://img.shields.io/packagist/v/matriphe/iso-639.svg)](https://packagist.org/packages/matriphe/iso-639) 6 | 7 | PHP library to convert ISO-639-1 code to language name, based on Wikipedia's [List of ISO 639-1 codes](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes). 8 | 9 | ## Installation 10 | 11 | For PHP 8.1 or latest: 12 | 13 | ```shell 14 | composer require matriphe/iso-639 15 | ``` 16 | 17 | For older PHP version: 18 | 19 | ```shell 20 | composer require matriphe/iso-639:1.3 21 | ``` 22 | 23 | ## Requirements 24 | 25 | - PHP 8.0 or higher 26 | - **Optional**: `ext-mbstring` extension for better multibyte character support 27 | 28 | The library will work without the `mbstring` extension, but for optimal handling of multibyte characters (like accented characters, Cyrillic, Arabic, etc.) in language names, it's recommended to install the `mbstring` extension. 29 | 30 | ## Usage Example 31 | 32 | ```php 33 | languageByCode1('en'); // English 42 | echo $iso->languageByCode1('id'); // Indonesian 43 | echo $iso->languageByCode1('jv'); // Javanese 44 | 45 | // Get native language name from ISO-639-1 code 46 | echo $iso->nativeByCode1('en'); // English 47 | echo $iso->nativeByCode1('id'); // Bahasa Indonesia 48 | echo $iso->nativeByCode1('jv'); // basa Jawa 49 | 50 | // Get native language name from ISO-639-1 code with capitalized 51 | echo $iso->nativeByCode1('en', true); // English 52 | echo $iso->nativeByCode1('id', true); // Bahasa Indonesia 53 | echo $iso->nativeByCode1('jv', true); // Basa Jawa 54 | echo $iso->nativeByCode1('hi', true); // हिन्दी, हिंदी 55 | echo $iso->nativeByCode1('th', true); // ไทย 56 | echo $iso->nativeByCode1('ko', true); // 한국어 57 | echo $iso->nativeByCode1('ja', true); // 日本語 (にほんご) 58 | echo $iso->nativeByCode1('zh', true); // '中文 (Zhōngwén), 汉语, 漢語 59 | echo $iso->nativeByCode1('ru', true); // Русский 60 | echo $iso->nativeByCode1('ar', true); // االعربية 61 | echo $iso->nativeByCode1('vi', true); // Việt Nam 62 | 63 | // Get language name from ISO-639-2t code 64 | echo $iso->languageByCode2t('eng'); // English 65 | echo $iso->languageByCode2t('ind'); // Indonesian 66 | echo $iso->languageByCode2t('jav'); // Javanese 67 | 68 | // Get native language name from ISO-639-2t code 69 | echo $iso->nativeByCode2t('eng'); // English 70 | echo $iso->nativeByCode2t('ind'); // Bahasa Indonesia 71 | echo $iso->nativeByCode2t('jav'); // basa Jawa 72 | 73 | // Get native language name from ISO-639-2t code with capitalized 74 | echo $iso->nativeByCode2t('eng', true); // English 75 | echo $iso->nativeByCode2t('ind', true); // Bahasa Indonesia 76 | echo $iso->nativeByCode2t('jav', true); // Basa Jawa 77 | echo $iso->nativeByCode2t('hin', true); // हिन्दी, हिंदी 78 | echo $iso->nativeByCode2t('tha', true); // ไทย 79 | echo $iso->nativeByCode2t('kor', true); // 한국어 80 | echo $iso->nativeByCode2t('jpn', true); // 日本語 (にほんご) 81 | echo $iso->nativeByCode2t('zho', true); // '中文 (Zhōngwén), 汉语, 漢語 82 | echo $iso->nativeByCode2t('rus', true); // Русский 83 | echo $iso->nativeByCode2t('ara', true); // االعربية 84 | echo $iso->nativeByCode2t('vie', true); // Việt Nam 85 | 86 | // Get language name from ISO-639-2b code 87 | echo $iso->languageByCode2b('eng'); // English 88 | echo $iso->languageByCode2b('ind'); // Indonesian 89 | echo $iso->languageByCode2b('jav'); // Javanese 90 | 91 | // Get native language name from ISO-639-2b code 92 | echo $iso->nativeByCode2b('eng'); // English 93 | echo $iso->nativeByCode2b('ind'); // Bahasa Indonesia 94 | echo $iso->nativeByCode2b('jav'); // basa Jawa 95 | 96 | // Get native language name from ISO-639-2b code with capitalized 97 | echo $iso->nativeByCode2b('eng', true); // English 98 | echo $iso->nativeByCode2b('ind', true); // Bahasa Indonesia 99 | echo $iso->nativeByCode2b('jav', true); // Basa Jawa 100 | echo $iso->nativeByCode2b('hin', true); // हिन्दी, हिंदी 101 | echo $iso->nativeByCode2b('tha', true); // ไทย 102 | echo $iso->nativeByCode2b('kor', true); // 한국어 103 | echo $iso->nativeByCode2b('jpn', true); // 日本語 (にほんご) 104 | echo $iso->nativeByCode2b('chi', true); // '中文 (Zhōngwén), 汉语, 漢語 105 | echo $iso->nativeByCode2b('rus', true); // Русский 106 | echo $iso->nativeByCode2b('ara', true); // االعربية 107 | echo $iso->nativeByCode2b('vie', true); // Việt Nam 108 | 109 | // Get language name from ISO-639-3 code 110 | echo $iso->languageByCode3('eng'); // English 111 | echo $iso->languageByCode3('ind'); // Indonesian 112 | echo $iso->languageByCode3('jav'); // Javanese 113 | 114 | // Get native language name from ISO-639-3 code 115 | echo $iso->nativeByCode3('eng'); // English 116 | echo $iso->nativeByCode3('ind'); // Bahasa Indonesia 117 | echo $iso->nativeByCode3('jav'); // basa Jawa 118 | 119 | // Get native language name from ISO-639-3 code with capitalized 120 | echo $iso->nativeByCode3('eng', true); // English 121 | echo $iso->nativeByCode3('ind', true); // Bahasa Indonesia 122 | echo $iso->nativeByCode3('jav', true); // Basa Jawa 123 | echo $iso->nativeByCode3('hin', true); // हिन्दी, हिंदी 124 | echo $iso->nativeByCode3('tha', true); // ไทย 125 | echo $iso->nativeByCode3('kor', true); // 한국어 126 | echo $iso->nativeByCode3('jpn', true); // 日本語 (にほんご) 127 | echo $iso->nativeByCode3('zho', true); // '中文 (Zhōngwén), 汉语, 漢語 128 | echo $iso->nativeByCode3('rus', true); // Русский 129 | echo $iso->nativeByCode3('ara', true); // االعربية 130 | echo $iso->nativeByCode3('vie', true); // Việt Nam 131 | 132 | // Get language array from ISO-639-2b code 133 | echo $iso->getLanguageByIsoCode2b('eng'); // ['en', 'eng', 'eng', 'eng', 'English', 'English'] 134 | echo $iso->getLanguageByIsoCode2b('ind'); // ['id', 'ind', 'ind', 'ind', 'Indonesian', 'Bahasa Indonesia'] 135 | echo $iso->getLanguageByIsoCode2b('jav'); // ['jv', 'jav', 'jav', 'jav', 'Javanese', 'basa Jawa'] 136 | ``` 137 | 138 | ## Contributing 139 | 140 | Do as usual contribution on open source projects by creating a pull request! 141 | 142 | ### Install Composer 143 | 144 | ```console 145 | composer install 146 | ``` 147 | 148 | ### Run Test 149 | 150 | ```console 151 | composer run-script test 152 | ``` 153 | 154 | ### Run Linter Fix 155 | 156 | ```console 157 | composer run-script lint:fix 158 | ``` 159 | -------------------------------------------------------------------------------- /resources/wikipedia.csv: -------------------------------------------------------------------------------- 1 | Northwest Caucasian|Abkhaz|аҧсуа бызшәа, аҧсшәа|ab|abk|abk|abk|abks| 2 | Afro-Asiatic|Afar|Afaraf|aa|aar|aar|aar|aars| 3 | Indo-European|Afrikaans|Afrikaans|af|afr|afr|afr|afrs| 4 | Niger–Congo|Akan|Akan|ak|aka|aka|aka + 2||macrolanguage, Twi is [tw/twi], Fanti is [fat] 5 | Indo-European|Albanian|Shqip|sq|sqi|alb|sqi + 4||macrolanguage, "Albanian Phylozone" in 639-6 6 | Afro-Asiatic|Amharic|አማርኛ|am|amh|amh|amh|| 7 | Afro-Asiatic|Arabic|العربية|ar|ara|ara|ara + 30||macrolanguage, Standard Arabic is [arb] 8 | Indo-European|Aragonese|aragonés|an|arg|arg|arg|| 9 | Indo-European|Armenian|Հայերեն|hy|hye|arm|hye|| 10 | Indo-European|Assamese|অসমীয়া|as|asm|asm|asm|| 11 | Northeast Caucasian|Avaric|авар мацӀ, магӀарул мацӀ|av|ava|ava|ava|| 12 | Indo-European|Avestan|avesta|ae|ave|ave|ave||ancient 13 | Aymaran|Aymara|aymar aru|ay|aym|aym|aym + 2||macrolanguage 14 | Turkic|Azerbaijani|azərbaycan dili|az|aze|aze|aze + 2||macrolanguage 15 | Niger–Congo|Bambara|bamanankan|bm|bam|bam|bam|| 16 | Turkic|Bashkir|башҡорт теле|ba|bak|bak|bak|| 17 | Language isolate|Basque|euskara, euskera|eu|eus|baq|eus|| 18 | Indo-European|Belarusian|беларуская мова|be|bel|bel|bel|| 19 | Indo-European|Bengali, Bangla|বাংলা|bn|ben|ben|ben|| 20 | Indo-European|Bihari|भोजपुरी|bh|bih|bih|||Collective language code for Bhojpuri, Magahi, and Maithili 21 | Creole|Bislama|Bislama|bi|bis|bis|bis|| 22 | Indo-European|Bosnian|bosanski jezik|bs|bos|bos|bos|boss| 23 | Indo-European|Breton|brezhoneg|br|bre|bre|bre|| 24 | Indo-European|Bulgarian|български език|bg|bul|bul|bul|buls| 25 | Sino-Tibetan|Burmese|ဗမာစာ|my|mya|bur|mya|| 26 | Indo-European|Catalan|català|ca|cat|cat|cat|| 27 | Austronesian|Chamorro|Chamoru|ch|cha|cha|cha|| 28 | Northeast Caucasian|Chechen|нохчийн мотт|ce|che|che|che|| 29 | Niger–Congo|Chichewa, Chewa, Nyanja|chiCheŵa, chinyanja|ny|nya|nya|nya|| 30 | Sino-Tibetan|Chinese|中文 (Zhōngwén), 汉语, 漢語|zh|zho|chi|zho + 13||macrolanguage 31 | Turkic|Chuvash|чӑваш чӗлхи|cv|chv|chv|chv|| 32 | Indo-European|Cornish|Kernewek|kw|cor|cor|cor|| 33 | Indo-European|Corsican|corsu, lingua corsa|co|cos|cos|cos|| 34 | Algonquian|Cree|ᓀᐦᐃᔭᐍᐏᐣ|cr|cre|cre|cre + 6||macrolanguage 35 | Indo-European|Croatian|hrvatski jezik|hr|hrv|hrv|hrv|| 36 | Indo-European|Czech|čeština, český jazyk|cs|ces|cze|ces|| 37 | Indo-European|Danish|dansk|da|dan|dan|dan|| 38 | Indo-European|Divehi, Dhivehi, Maldivian|ދިވެހި|dv|div|div|div|| 39 | Indo-European|Dutch|Nederlands, Vlaams|nl|nld|dut|nld|| 40 | Sino-Tibetan|Dzongkha|རྫོང་ཁ|dz|dzo|dzo|dzo|| 41 | Indo-European|English|English|en|eng|eng|eng|engs| 42 | Constructed|Esperanto|Esperanto|eo|epo|epo|epo||constructed, initiated from L.L. Zamenhof, 1887 43 | Uralic|Estonian|eesti, eesti keel|et|est|est|est + 2||macrolanguage 44 | Niger–Congo|Ewe|Eʋegbe|ee|ewe|ewe|ewe|| 45 | Indo-European|Faroese|føroyskt|fo|fao|fao|fao|| 46 | Austronesian|Fijian|vosa Vakaviti|fj|fij|fij|fij|| 47 | Uralic|Finnish|suomi, suomen kieli|fi|fin|fin|fin|| 48 | Indo-European|French|français, langue française|fr|fra|fre|fra|fras| 49 | Niger–Congo|Fula, Fulah, Pulaar, Pular|Fulfulde, Pulaar, Pular|ff|ful|ful|ful + 9||macrolanguage 50 | Indo-European|Galician|galego|gl|glg|glg|glg|| 51 | South Caucasian|Georgian|ქართული|ka|kat|geo|kat|| 52 | Indo-European|German|Deutsch|de|deu|ger|deu|deus| 53 | Indo-European|Greek (modern)|ελληνικά|el|ell|gre|ell|ells| 54 | Tupian|Guaraní|Avañe'ẽ|gn|grn|grn|grn + 5||macrolanguage 55 | Indo-European|Gujarati|ગુજરાતી|gu|guj|guj|guj|| 56 | Creole|Haitian, Haitian Creole|Kreyòl ayisyen|ht|hat|hat|hat|| 57 | Afro-Asiatic|Hausa|(Hausa) هَوُسَ|ha|hau|hau|hau|| 58 | Afro-Asiatic|Hebrew (modern)|עברית|he|heb|heb|heb|| 59 | Niger–Congo|Herero|Otjiherero|hz|her|her|her|| 60 | Indo-European|Hindi|हिन्दी, हिंदी|hi|hin|hin|hin|hins| 61 | Austronesian|Hiri Motu|Hiri Motu|ho|hmo|hmo|hmo|| 62 | Uralic|Hungarian|magyar|hu|hun|hun|hun|| 63 | Constructed|Interlingua|Interlingua|ia|ina|ina|ina||constructed by International Auxiliary Language Association 64 | Austronesian|Indonesian|Bahasa Indonesia|id|ind|ind|ind||Covered by macrolanguage [ms/msa] 65 | Constructed|Interlingue|Originally called Occidental; then Interlingue after WWII|ie|ile|ile|ile||constructed by Edgar de Wahl, first published in 1922 66 | Indo-European|Irish|Gaeilge|ga|gle|gle|gle|| 67 | Niger–Congo|Igbo|Asụsụ Igbo|ig|ibo|ibo|ibo|| 68 | Eskimo–Aleut|Inupiaq|Iñupiaq, Iñupiatun|ik|ipk|ipk|ipk + 2||macrolanguage 69 | Constructed|Ido|Ido|io|ido|ido|ido|idos|constructed by De Beaufront, 1907, as variation of Esperanto 70 | Indo-European|Icelandic|Íslenska|is|isl|ice|isl|| 71 | Indo-European|Italian|italiano|it|ita|ita|ita|itas| 72 | Eskimo–Aleut|Inuktitut|ᐃᓄᒃᑎᑐᑦ|iu|iku|iku|iku + 2||macrolanguage 73 | Japonic|Japanese|日本語 (にほんご)|ja|jpn|jpn|jpn|| 74 | Austronesian|Javanese|basa Jawa|jv|jav|jav|jav|| 75 | Eskimo–Aleut|Kalaallisut, Greenlandic|kalaallisut, kalaallit oqaasii|kl|kal|kal|kal|| 76 | Dravidian|Kannada|ಕನ್ನಡ|kn|kan|kan|kan|| 77 | Nilo-Saharan|Kanuri|Kanuri|kr|kau|kau|kau + 3||macrolanguage 78 | Indo-European|Kashmiri|कश्मीरी, كشميري‎|ks|kas|kas|kas|| 79 | Turkic|Kazakh|қазақ тілі|kk|kaz|kaz|kaz|| 80 | Austroasiatic|Khmer|ខ្មែរ, ខេមរភាសា, ភាសាខ្មែរ|km|khm|khm|khm||a.k.a. Cambodian 81 | Niger–Congo|Kikuyu, Gikuyu|Gĩkũyũ|ki|kik|kik|kik|| 82 | Niger–Congo|Kinyarwanda|Ikinyarwanda|rw|kin|kin|kin|| 83 | Turkic|Kyrgyz|Кыргызча, Кыргыз тили|ky|kir|kir|kir|| 84 | Uralic|Komi|коми кыв|kv|kom|kom|kom + 2||macrolanguage 85 | Niger–Congo|Kongo|Kikongo|kg|kon|kon|kon + 3||macrolanguage 86 | Koreanic|Korean|한국어, 조선어|ko|kor|kor|kor|| 87 | Indo-European|Kurdish|Kurdî, كوردی‎|ku|kur|kur|kur + 3||macrolanguage 88 | Niger–Congo|Kwanyama, Kuanyama|Kuanyama|kj|kua|kua|kua|| 89 | Indo-European|Latin|latine, lingua latina|la|lat|lat|lat|lats|ancient 90 | Indo-European|Ladin|ladin, lingua ladina||||lld|| 91 | Indo-European|Luxembourgish, Letzeburgesch|Lëtzebuergesch|lb|ltz|ltz|ltz|| 92 | Niger–Congo|Ganda|Luganda|lg|lug|lug|lug|| 93 | Indo-European|Limburgish, Limburgan, Limburger|Limburgs|li|lim|lim|lim|| 94 | Niger–Congo|Lingala|Lingála|ln|lin|lin|lin|| 95 | Tai–Kadai|Lao|ພາສາລາວ|lo|lao|lao|lao|| 96 | Indo-European|Lithuanian|lietuvių kalba|lt|lit|lit|lit|| 97 | Niger–Congo|Luba-Katanga|Tshiluba|lu|lub|lub|lub|| 98 | Indo-European|Latvian|latviešu valoda|lv|lav|lav|lav + 2||macrolanguage 99 | Indo-European|Manx|Gaelg, Gailck|gv|glv|glv|glv|| 100 | Indo-European|Macedonian|македонски јазик|mk|mkd|mac|mkd|| 101 | Austronesian|Malagasy|fiteny malagasy|mg|mlg|mlg|mlg + 10||macrolanguage 102 | Austronesian|Malay|bahasa Melayu, بهاس ملايو‎|ms|msa|may|msa + 13||macrolanguage, Standard Malay is [zsm], Indonesian is [id/ind] 103 | Dravidian|Malayalam|മലയാളം|ml|mal|mal|mal|| 104 | Afro-Asiatic|Maltese|Malti|mt|mlt|mlt|mlt|| 105 | Austronesian|Māori|te reo Māori|mi|mri|mao|mri|| 106 | Indo-European|Marathi (Marāṭhī)|मराठी|mr|mar|mar|mar|| 107 | Austronesian|Marshallese|Kajin M̧ajeļ|mh|mah|mah|mah|| 108 | Mongolic|Mongolian|монгол|mn|mon|mon|mon + 2||macrolanguage 109 | Austronesian|Nauru|Ekakairũ Naoero|na|nau|nau|nau|| 110 | Dené–Yeniseian|Navajo, Navaho|Diné bizaad|nv|nav|nav|nav|| 111 | Niger–Congo|Northern Ndebele|isiNdebele|nd|nde|nde|nde|| 112 | Indo-European|Nepali|नेपाली|ne|nep|nep|nep|| 113 | Niger–Congo|Ndonga|Owambo|ng|ndo|ndo|ndo|| 114 | Indo-European|Norwegian Bokmål|Norsk bokmål|nb|nob|nob|nob||Covered by macrolanguage [no/nor] 115 | Indo-European|Norwegian Nynorsk|Norsk nynorsk|nn|nno|nno|nno||Covered by macrolanguage [no/nor] 116 | Indo-European|Norwegian|Norsk|no|nor|nor|nor + 2||macrolanguage, Bokmål is [nb/nob], Nynorsk is [nn/nno] 117 | Sino-Tibetan|Nuosu|ꆈꌠ꒿ Nuosuhxop|ii|iii|iii|iii||Standard form of Yi languages 118 | Niger–Congo|Southern Ndebele|isiNdebele|nr|nbl|nbl|nbl|| 119 | Indo-European|Occitan|occitan, lenga d'òc|oc|oci|oci|oci|| 120 | Algonquian|Ojibwe, Ojibwa|ᐊᓂᔑᓈᐯᒧᐎᓐ|oj|oji|oji|oji + 7||macrolanguage 121 | Indo-European|Old Church Slavonic, Church Slavonic, Old Bulgarian|ѩзыкъ словѣньскъ|cu|chu|chu|chu||ancient, in use by Orthodox Church 122 | Afro-Asiatic|Oromo|Afaan Oromoo|om|orm|orm|orm + 4||macrolanguage 123 | Indo-European|Oriya|ଓଡ଼ିଆ|or|ori|ori|ori|| 124 | Indo-European|Ossetian, Ossetic|ирон æвзаг|os|oss|oss|oss|| 125 | Indo-European|Panjabi, Punjabi|ਪੰਜਾਬੀ, پنجابی‎|pa|pan|pan|pan|| 126 | Indo-European|Pāli|पाऴि|pi|pli|pli|pli||ancient 127 | Indo-European|Persian (Farsi)|فارسی|fa|fas|per|fas + 2||macrolanguage 128 | Indo-European|Polish|język polski, polszczyzna|pl|pol|pol|pol|pols| 129 | Indo-European|Pashto, Pushto|پښتو|ps|pus|pus|pus + 3||macrolanguage 130 | Indo-European|Portuguese|português|pt|por|por|por|| 131 | Quechuan|Quechua|Runa Simi, Kichwa|qu|que|que|que + 44||macrolanguage 132 | Indo-European|Romansh|rumantsch grischun|rm|roh|roh|roh|| 133 | Niger–Congo|Kirundi|Ikirundi|rn|run|run|run|| 134 | Indo-European|Romanian|limba română|ro|ron|rum|ron||[mo] for Moldavian has been withdrawn, recommending [ro] also for Moldavian 135 | Indo-European|Russian|Русский|ru|rus|rus|rus|| 136 | Indo-European|Sanskrit (Saṁskṛta)|संस्कृतम्|sa|san|san|san||ancient, still spoken 137 | Indo-European|Sardinian|sardu|sc|srd|srd|srd + 4||macrolanguage 138 | Indo-European|Sindhi|सिन्धी, سنڌي، سندھی‎|sd|snd|snd|snd|| 139 | Uralic|Northern Sami|Davvisámegiella|se|sme|sme|sme|| 140 | Austronesian|Samoan|gagana fa'a Samoa|sm|smo|smo|smo|| 141 | Creole|Sango|yângâ tî sängö|sg|sag|sag|sag|| 142 | Indo-European|Serbian|српски језик|sr|srp|srp|srp||The ISO 639-2/T code srp deprecated the ISO 639-2/B code scc[1] 143 | Indo-European|Scottish Gaelic, Gaelic|Gàidhlig|gd|gla|gla|gla|| 144 | Niger–Congo|Shona|chiShona|sn|sna|sna|sna|| 145 | Indo-European|Sinhala, Sinhalese|සිංහල|si|sin|sin|sin|| 146 | Indo-European|Slovak|slovenčina, slovenský jazyk|sk|slk|slo|slk|| 147 | Indo-European|Slovene|slovenski jezik, slovenščina|sl|slv|slv|slv|| 148 | Afro-Asiatic|Somali|Soomaaliga, af Soomaali|so|som|som|som|| 149 | Niger–Congo|Southern Sotho|Sesotho|st|sot|sot|sot|| 150 | Indo-European|Spanish|español|es|spa|spa|spa|| 151 | Austronesian|Sundanese|Basa Sunda|su|sun|sun|sun|| 152 | Niger–Congo|Swahili|Kiswahili|sw|swa|swa|swa + 2||macrolanguage 153 | Niger–Congo|Swati|SiSwati|ss|ssw|ssw|ssw|| 154 | Indo-European|Swedish|svenska|sv|swe|swe|swe|| 155 | Dravidian|Tamil|தமிழ்|ta|tam|tam|tam|| 156 | Dravidian|Telugu|తెలుగు|te|tel|tel|tel|| 157 | Indo-European|Tajik|тоҷикӣ, toçikī, تاجیکی‎|tg|tgk|tgk|tgk|| 158 | Tai–Kadai|Thai|ไทย|th|tha|tha|tha|| 159 | Afro-Asiatic|Tigrinya|ትግርኛ|ti|tir|tir|tir|| 160 | Sino-Tibetan|Tibetan Standard, Tibetan, Central|བོད་ཡིག|bo|bod|tib|bod|| 161 | Turkic|Turkmen|Türkmen, Түркмен|tk|tuk|tuk|tuk|| 162 | Austronesian|Tagalog|Wikang Tagalog, ᜏᜒᜃᜅ᜔ ᜆᜄᜎᜓᜄ᜔|tl|tgl|tgl|tgl||Note: Filipino (Pilipino) has the code [fil] 163 | Niger–Congo|Tswana|Setswana|tn|tsn|tsn|tsn|| 164 | Austronesian|Tonga (Tonga Islands)|faka Tonga|to|ton|ton|ton|| 165 | Turkic|Turkish|Türkçe|tr|tur|tur|tur|| 166 | Niger–Congo|Tsonga|Xitsonga|ts|tso|tso|tso|| 167 | Turkic|Tatar|татар теле, tatar tele|tt|tat|tat|tat|| 168 | Niger–Congo|Twi|Twi|tw|twi|twi|twi||Covered by macrolanguage [ak/aka] 169 | Austronesian|Tahitian|Reo Tahiti|ty|tah|tah|tah||One of the Reo Mā`ohi (languages of French Polynesia) 170 | Turkic|Uyghur|ئۇيغۇرچە‎, Uyghurche|ug|uig|uig|uig|| 171 | Indo-European|Ukrainian|українська мова|uk|ukr|ukr|ukr|| 172 | Indo-European|Urdu|اردو|ur|urd|urd|urd|| 173 | Turkic|Uzbek|Oʻzbek, Ўзбек, أۇزبېك‎|uz|uzb|uzb|uzb + 2||macrolanguage 174 | Niger–Congo|Venda|Tshivenḓa|ve|ven|ven|ven|| 175 | Austroasiatic|Vietnamese|Việt Nam|vi|vie|vie|vie|| 176 | Constructed|Volapük|Volapük|vo|vol|vol|vol||constructed 177 | Indo-European|Walloon|walon|wa|wln|wln|wln|| 178 | Indo-European|Welsh|Cymraeg|cy|cym|wel|cym|| 179 | Niger–Congo|Wolof|Wollof|wo|wol|wol|wol|| 180 | Indo-European|Western Frisian|Frysk|fy|fry|fry|fry|| 181 | Niger–Congo|Xhosa|isiXhosa|xh|xho|xho|xho|| 182 | Indo-European|Yiddish|ייִדיש|yi|yid|yid|yid + 2||macrolanguage 183 | Niger–Congo|Yoruba|Yorùbá|yo|yor|yor|yor|| 184 | Tai–Kadai|Zhuang, Chuang|Saɯ cueŋƅ, Saw cuengh|za|zha|zha|zha + 16||macrolanguage 185 | Niger–Congo|Zulu|isiZulu|zu|zul|zul|zul|| -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Create Release 2 | 3 | on: 4 | workflow_run: 5 | workflows: 6 | - Run Test 7 | types: 8 | - completed 9 | branches: 10 | - master 11 | workflow_dispatch: 12 | 13 | permissions: 14 | contents: write 15 | pull-requests: read 16 | 17 | jobs: 18 | create-release: 19 | runs-on: ubuntu-latest 20 | # Only run if the test workflow completed successfully 21 | if: github.event.workflow_run.conclusion == 'success' 22 | 23 | steps: 24 | - name: Checkout code 25 | uses: actions/checkout@v5 26 | with: 27 | fetch-depth: 0 28 | token: ${{ secrets.GITHUB_TOKEN }} 29 | ref: ${{ github.event.workflow_run.head_sha }} 30 | 31 | - name: Check if triggered by PR merge to master 32 | id: check-trigger 33 | run: | 34 | # Check if this was triggered by a PR merge to master 35 | # workflow_run events don't have direct PR info, so we check the commit message 36 | COMMIT_MESSAGE=$(git log -1 --pretty=%B) 37 | echo "Commit message: $COMMIT_MESSAGE" 38 | 39 | # Extract PR number from merge commit message 40 | if echo "$COMMIT_MESSAGE" | grep -q "Merge pull request"; then 41 | echo "Triggered by PR merge to master" 42 | echo "is-pr-merge=true" >> $GITHUB_OUTPUT 43 | 44 | # Extract PR number from commit message like "Merge pull request #123 from..." 45 | PR_NUMBER=$(echo "$COMMIT_MESSAGE" | sed -n 's/.*Merge pull request #\([0-9]*\).*/\1/p') 46 | echo "pr-number=$PR_NUMBER" >> $GITHUB_OUTPUT 47 | echo "Found PR number: $PR_NUMBER" 48 | else 49 | echo "Not triggered by PR merge, skipping release" 50 | echo "is-pr-merge=false" >> $GITHUB_OUTPUT 51 | fi 52 | 53 | - name: Get PR description 54 | id: get-pr-description 55 | if: steps.check-trigger.outputs.is-pr-merge == 'true' 56 | env: 57 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 58 | run: | 59 | # Install jq if not available 60 | which jq || sudo apt-get update && sudo apt-get install -y jq 61 | 62 | PR_NUMBER="${{ steps.check-trigger.outputs.pr-number }}" 63 | 64 | if [ -n "$PR_NUMBER" ]; then 65 | echo "Fetching PR #$PR_NUMBER description..." 66 | 67 | # Get PR description using GitHub API 68 | PR_DESCRIPTION=$(curl -s -H "Authorization: token $GITHUB_TOKEN" \ 69 | "https://api.github.com/repos/${{ github.repository }}/pulls/$PR_NUMBER" | \ 70 | jq -r '.body // "No description provided"') 71 | 72 | echo "PR Description:" 73 | echo "$PR_DESCRIPTION" 74 | 75 | # Save PR description for later use 76 | echo "pr-description<> $GITHUB_OUTPUT 77 | echo "$PR_DESCRIPTION" >> $GITHUB_OUTPUT 78 | echo "EOF" >> $GITHUB_OUTPUT 79 | else 80 | echo "No PR number found" 81 | echo "pr-description=No description available" >> $GITHUB_OUTPUT 82 | fi 83 | 84 | - name: Check if changes are only in workflow files 85 | id: check-changes 86 | if: steps.check-trigger.outputs.is-pr-merge == 'true' 87 | run: | 88 | # Get the list of changed files in the last commit 89 | CHANGED_FILES=$(git diff --name-only HEAD~1 HEAD) 90 | 91 | # Check if all changed files are in .github/workflows/ directory 92 | WORKFLOW_ONLY=true 93 | for file in $CHANGED_FILES; do 94 | if [[ ! "$file" =~ ^\.github/workflows/ ]]; then 95 | WORKFLOW_ONLY=false 96 | break 97 | fi 98 | done 99 | 100 | echo "workflow-only=$WORKFLOW_ONLY" >> $GITHUB_OUTPUT 101 | echo "Changed files: $CHANGED_FILES" 102 | 103 | # If only workflow files changed, skip release 104 | if [ "$WORKFLOW_ONLY" = "true" ]; then 105 | echo "Only workflow files changed, skipping release" 106 | echo "skip-release=true" >> $GITHUB_OUTPUT 107 | else 108 | echo "Non-workflow files changed, proceeding with release" 109 | echo "skip-release=false" >> $GITHUB_OUTPUT 110 | fi 111 | 112 | - name: Get latest release 113 | id: get-latest-release 114 | if: steps.check-trigger.outputs.is-pr-merge == 'true' && steps.check-changes.outputs.skip-release == 'false' 115 | run: | 116 | # Get the latest release tag 117 | LATEST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "") 118 | if [ -z "$LATEST_TAG" ]; then 119 | echo "No previous releases found, starting from v0.0.0" 120 | echo "latest-tag=v0.0.0" >> $GITHUB_OUTPUT 121 | echo "is-first-release=true" >> $GITHUB_OUTPUT 122 | else 123 | echo "Latest tag: $LATEST_TAG" 124 | echo "latest-tag=$LATEST_TAG" >> $GITHUB_OUTPUT 125 | echo "is-first-release=false" >> $GITHUB_OUTPUT 126 | fi 127 | 128 | - name: Determine version bump 129 | id: version-bump 130 | if: steps.check-trigger.outputs.is-pr-merge == 'true' && steps.check-changes.outputs.skip-release == 'false' 131 | run: | 132 | # Get the branch name from the PR 133 | PR_NUMBER="${{ steps.check-trigger.outputs.pr-number }}" 134 | BRANCH_NAME="" 135 | 136 | if [ -n "$PR_NUMBER" ]; then 137 | # Get branch name from GitHub API 138 | BRANCH_NAME=$(curl -s -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \ 139 | "https://api.github.com/repos/${{ github.repository }}/pulls/$PR_NUMBER" | \ 140 | jq -r '.head.ref // ""') 141 | echo "Branch name: $BRANCH_NAME" 142 | fi 143 | 144 | # Initialize version bump flags 145 | MAJOR_BUMP=false 146 | MINOR_BUMP=false 147 | PATCH_BUMP=false 148 | 149 | # Check branch name for explicit version bump indicators 150 | if [ -n "$BRANCH_NAME" ]; then 151 | echo "Analyzing branch name: $BRANCH_NAME" 152 | 153 | # Convert to lowercase for case-insensitive matching 154 | BRANCH_LOWER=$(echo "$BRANCH_NAME" | tr '[:upper:]' '[:lower:]') 155 | 156 | # Check for major version indicators in branch name 157 | if echo "$BRANCH_LOWER" | grep -qE "(major|refactor|rewrite|breaking)"; then 158 | echo "🔴 MAJOR version bump detected from branch name" 159 | MAJOR_BUMP=true 160 | # Check for minor version indicators in branch name 161 | elif echo "$BRANCH_LOWER" | grep -qE "(minor|feature|feat|new)"; then 162 | echo "🟡 MINOR version bump detected from branch name" 163 | MINOR_BUMP=true 164 | # Check for patch version indicators in branch name 165 | elif echo "$BRANCH_LOWER" | grep -qE "(patch|fix|bug|hotfix|chore|docs|style|test)"; then 166 | echo "🟢 PATCH version bump detected from branch name" 167 | PATCH_BUMP=true 168 | else 169 | # If no keywords found in branch name, default to patch 170 | echo "🟢 No version keywords found in branch name, defaulting to PATCH version bump" 171 | PATCH_BUMP=true 172 | fi 173 | else 174 | # If no branch name found, default to patch 175 | echo "🟢 No branch name found, defaulting to PATCH version bump" 176 | PATCH_BUMP=true 177 | fi 178 | 179 | echo "Version bump decision:" 180 | echo " Branch name: $BRANCH_NAME" 181 | echo " Major bump: $MAJOR_BUMP" 182 | echo " Minor bump: $MINOR_BUMP" 183 | echo " Patch bump: $PATCH_BUMP" 184 | 185 | echo "major-bump=$MAJOR_BUMP" >> $GITHUB_OUTPUT 186 | echo "minor-bump=$MINOR_BUMP" >> $GITHUB_OUTPUT 187 | echo "patch-bump=$PATCH_BUMP" >> $GITHUB_OUTPUT 188 | 189 | - name: Calculate new version 190 | id: calculate-version 191 | if: steps.check-trigger.outputs.is-pr-merge == 'true' && steps.check-changes.outputs.skip-release == 'false' 192 | run: | 193 | LATEST_TAG="${{ steps.get-latest-release.outputs.latest-tag }}" 194 | 195 | # Extract version numbers 196 | if [ "${{ steps.get-latest-release.outputs.is-first-release }}" = "true" ]; then 197 | NEW_VERSION="v1.0.0" 198 | else 199 | # Remove 'v' prefix and split version 200 | VERSION=${LATEST_TAG#v} 201 | IFS='.' read -r MAJOR MINOR PATCH <<< "$VERSION" 202 | 203 | if [ "${{ steps.version-bump.outputs.major-bump }}" = "true" ]; then 204 | MAJOR=$((MAJOR + 1)) 205 | MINOR=0 206 | PATCH=0 207 | elif [ "${{ steps.version-bump.outputs.minor-bump }}" = "true" ]; then 208 | MINOR=$((MINOR + 1)) 209 | PATCH=0 210 | elif [ "${{ steps.version-bump.outputs.patch-bump }}" = "true" ]; then 211 | PATCH=$((PATCH + 1)) 212 | fi 213 | 214 | NEW_VERSION="v${MAJOR}.${MINOR}.${PATCH}" 215 | fi 216 | 217 | echo "new-version=$NEW_VERSION" >> $GITHUB_OUTPUT 218 | echo "New version: $NEW_VERSION" 219 | 220 | - name: Generate release description 221 | id: release-description 222 | if: steps.check-trigger.outputs.is-pr-merge == 'true' && steps.check-changes.outputs.skip-release == 'false' 223 | run: | 224 | PR_DESCRIPTION="${{ steps.get-pr-description.outputs.pr-description }}" 225 | LATEST_TAG="${{ steps.get-latest-release.outputs.latest-tag }}" 226 | NEW_VERSION="${{ steps.calculate-version.outputs.new-version }}" 227 | 228 | # Create release description with PR description and changelog 229 | echo "## Release Notes" > RELEASE_DESCRIPTION.md 230 | echo "" >> RELEASE_DESCRIPTION.md 231 | 232 | if [ "$PR_DESCRIPTION" != "No description provided" ] && [ "$PR_DESCRIPTION" != "No description available" ] && [ -n "$PR_DESCRIPTION" ]; then 233 | echo "$PR_DESCRIPTION" >> RELEASE_DESCRIPTION.md 234 | echo "" >> RELEASE_DESCRIPTION.md 235 | fi 236 | 237 | # Add version bump information 238 | echo "## Version Information" >> RELEASE_DESCRIPTION.md 239 | echo "" >> RELEASE_DESCRIPTION.md 240 | if [ "${{ steps.version-bump.outputs.major-bump }}" = "true" ]; then 241 | echo "🔴 **Major Release** - Contains breaking changes or major refactoring" >> RELEASE_DESCRIPTION.md 242 | elif [ "${{ steps.version-bump.outputs.minor-bump }}" = "true" ]; then 243 | echo "🟡 **Minor Release** - Contains new features" >> RELEASE_DESCRIPTION.md 244 | elif [ "${{ steps.version-bump.outputs.patch-bump }}" = "true" ]; then 245 | echo "🟢 **Patch Release** - Contains bug fixes and improvements" >> RELEASE_DESCRIPTION.md 246 | fi 247 | echo "" >> RELEASE_DESCRIPTION.md 248 | 249 | echo "## Changes" >> RELEASE_DESCRIPTION.md 250 | echo "" >> RELEASE_DESCRIPTION.md 251 | 252 | # Generate changelog from commits 253 | if [ "${{ steps.get-latest-release.outputs.is-first-release }}" = "true" ]; then 254 | COMMITS=$(git log --oneline --no-merges --reverse) 255 | echo "### Initial Release" >> RELEASE_DESCRIPTION.md 256 | echo "" >> RELEASE_DESCRIPTION.md 257 | echo "$COMMITS" >> RELEASE_DESCRIPTION.md 258 | else 259 | COMMITS=$(git log --oneline --no-merges --reverse ${LATEST_TAG}..HEAD) 260 | echo "$COMMITS" >> RELEASE_DESCRIPTION.md 261 | fi 262 | 263 | # Read release description for output 264 | RELEASE_DESC=$(cat RELEASE_DESCRIPTION.md) 265 | echo "release-description<> $GITHUB_OUTPUT 266 | echo "$RELEASE_DESC" >> $GITHUB_OUTPUT 267 | echo "EOF" >> $GITHUB_OUTPUT 268 | 269 | echo "Final release description:" 270 | echo "$RELEASE_DESC" 271 | 272 | - name: Create Release 273 | if: steps.check-trigger.outputs.is-pr-merge == 'true' && steps.check-changes.outputs.skip-release == 'false' 274 | uses: softprops/action-gh-release@v1 275 | env: 276 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 277 | with: 278 | tag_name: ${{ steps.calculate-version.outputs.new-version }} 279 | name: ${{ steps.calculate-version.outputs.new-version }} 280 | body: ${{ steps.release-description.outputs.release-description }} 281 | draft: false 282 | prerelease: false 283 | 284 | - name: Skip Release - Not PR Merge 285 | if: steps.check-trigger.outputs.is-pr-merge == 'false' 286 | run: | 287 | echo "Skipping release as this was not triggered by a PR merge" 288 | 289 | - name: Skip Release - Workflow Only 290 | if: steps.check-trigger.outputs.is-pr-merge == 'true' && steps.check-changes.outputs.skip-release == 'true' 291 | run: | 292 | echo "Skipping release as only workflow files were changed" 293 | -------------------------------------------------------------------------------- /src/ISO639.php: -------------------------------------------------------------------------------- 1 | hasMbstring = extension_loaded('mbstring'); 225 | 226 | $this->buildHashmap(); 227 | } 228 | 229 | /** 230 | * Convert string to lowercase using mbstring if available, fallback to strtolower 231 | */ 232 | private function toLower(string $string): string 233 | { 234 | $string = trim($string); 235 | 236 | if ($this->hasMbstring) { 237 | return mb_strtolower($string, 'UTF-8'); 238 | } 239 | 240 | return strtolower($string); 241 | } 242 | 243 | /** 244 | * Convert first character of each word to uppercase using mbstring if available, fallback to ucwords 245 | */ 246 | private function toTitleCase(string $string): string 247 | { 248 | if ($this->hasMbstring) { 249 | return mb_convert_case($string, MB_CASE_TITLE, 'UTF-8'); 250 | } 251 | 252 | return ucwords($string); 253 | } 254 | 255 | private array $iso639_1 = []; 256 | private array $iso639_2t = []; 257 | private array $iso639_2b = []; 258 | private array $iso639_3 = []; 259 | private array $langEnglish = []; 260 | private array $code2tToCode1 = []; 261 | private array $code1ToCode2t = []; 262 | private array $code2bToLang = []; 263 | 264 | private function buildHashmap(): void 265 | { 266 | foreach ($this->languages as $lang) { 267 | $iso639_1 = $this->toLower($lang[self::INDEX_ISO639_1]); 268 | $iso639_2t = $this->toLower($lang[self::INDEX_ISO639_2T]); 269 | $iso639_2b = $this->toLower($lang[self::INDEX_ISO639_2B]); 270 | $iso639_3 = $this->toLower($lang[self::INDEX_ISO639_3]); 271 | 272 | $english = $lang[self::INDEX_ENGLISH_NAME]; 273 | $native = $lang[self::INDEX_NATIVE_NAME]; 274 | 275 | $val = [ 276 | self::KEY_CODE_1 => $iso639_1, 277 | self::KEY_CODE_2T => $iso639_2t, 278 | self::KEY_CODE_2B => $iso639_2b, 279 | self::KEY_CODE_3 => $iso639_3, 280 | self::KEY_ENGLISH => $english, 281 | self::KEY_NATIVE => $native, 282 | ]; 283 | 284 | if (!empty($iso639_1)) { 285 | $this->iso639_1[$iso639_1] = $val; 286 | } 287 | if (!empty($iso639_2t)) { 288 | $this->iso639_2t[$iso639_2t] = $val; 289 | } 290 | if (!empty($iso639_2b)) { 291 | $this->iso639_2b[$iso639_2b] = $val; 292 | 293 | $this->code2bToLang[$iso639_2b] = $lang; 294 | } 295 | if (!empty($iso639_3)) { 296 | $this->iso639_3[$iso639_3] = $val; 297 | } 298 | 299 | $this->langEnglish[$this->toLower($english)] = $val; 300 | 301 | if (!empty($iso639_2t) && !empty($iso639_1)) { 302 | $this->code2tToCode1[$iso639_2t] = $iso639_1; 303 | } 304 | if (!empty($iso639_1) && !empty($iso639_2t)) { 305 | $this->code1ToCode2t[$iso639_1] = $iso639_2t; 306 | } 307 | } 308 | } 309 | 310 | /* 311 | * Get all language data 312 | */ 313 | public function allLanguages(): array 314 | { 315 | return $this->languages; 316 | } 317 | 318 | /* 319 | * Get language name from ISO-639-1 (two-letters code) 320 | */ 321 | public function languageByCode1(string $code): string 322 | { 323 | return $this->iso639_1[$this->toLower($code)][self::KEY_ENGLISH] ?? ''; 324 | } 325 | 326 | /* 327 | * Get native language name from ISO-639-1 (two-letters code) 328 | */ 329 | public function nativeByCode1(string $code, bool $isCapitalized = false): string 330 | { 331 | $native = $this->iso639_1[$this->toLower($code)][self::KEY_NATIVE] ?? ''; 332 | 333 | return $isCapitalized ? $this->toTitleCase($native) : $native; 334 | } 335 | 336 | /* 337 | * Get language name from ISO-639-2/t (three-letter codes) terminologic 338 | */ 339 | public function languageByCode2t(string $code): string 340 | { 341 | return $this->iso639_2t[$this->toLower($code)][self::KEY_ENGLISH] ?? ''; 342 | } 343 | 344 | /* 345 | * Get native language name from ISO-639-2/t (three-letter codes) terminologic 346 | */ 347 | public function nativeByCode2t(string $code, bool $isCapitalized = false): string 348 | { 349 | $native = $this->iso639_2t[$this->toLower($code)][self::KEY_NATIVE] ?? ''; 350 | 351 | return $isCapitalized ? $this->toTitleCase($native) : $native; 352 | } 353 | 354 | /* 355 | * Get language name from ISO-639-2/b (three-letter codes) bibliographic 356 | */ 357 | public function languageByCode2b(string $code): string 358 | { 359 | return $this->iso639_2b[$this->toLower($code)][self::KEY_ENGLISH] ?? ''; 360 | } 361 | 362 | /* 363 | * Get native language name from ISO-639-2/b (three-letter codes) bibliographic 364 | */ 365 | public function nativeByCode2b(string $code, bool $isCapitalized = false): string 366 | { 367 | $native = $this->iso639_2b[$this->toLower($code)][self::KEY_NATIVE] ?? ''; 368 | 369 | return $isCapitalized ? $this->toTitleCase($native) : $native; 370 | } 371 | 372 | /* 373 | * Get language name from ISO-639-3 (three-letter codes) 374 | */ 375 | public function languageByCode3($code): string 376 | { 377 | return $this->iso639_3[$this->toLower($code)][self::KEY_ENGLISH] ?? ''; 378 | } 379 | 380 | /* 381 | * Get native language name from ISO-639-3 (three-letter codes) 382 | */ 383 | public function nativeByCode3(string $code, bool $isCapitalized = false): string 384 | { 385 | $native = $this->iso639_3[$this->toLower($code)][self::KEY_NATIVE] ?? ''; 386 | 387 | return $isCapitalized ? $this->toTitleCase($native) : $native; 388 | } 389 | 390 | /* 391 | * Get ISO-639-1 (two-letters code) from language name 392 | */ 393 | public function code1ByLanguage(string $language): string 394 | { 395 | return $this->langEnglish[$this->toLower($language)][self::KEY_CODE_1] ?? ''; 396 | } 397 | 398 | /* 399 | * Get ISO-639-2/t (three-letter codes) terminologic from language name 400 | */ 401 | public function code2tByLanguage(string $language): string 402 | { 403 | return $this->langEnglish[$this->toLower($language)][self::KEY_CODE_2T] ?? ''; 404 | } 405 | 406 | /* 407 | * Get ISO-639-2/b (three-letter codes) bibliographic from language name 408 | */ 409 | public function code2bByLanguage(string $language): string 410 | { 411 | return $this->langEnglish[$this->toLower($language)][self::KEY_CODE_2B] ?? ''; 412 | } 413 | 414 | /* 415 | * Get ISO-639-3 (three-letter codes) from language name 416 | */ 417 | public function code3ByLanguage(string $language): string 418 | { 419 | return $this->langEnglish[$this->toLower($language)][self::KEY_CODE_3] ?? ''; 420 | } 421 | 422 | /** 423 | * Gat language array from ISO-639-2/b (three-letter code) 424 | */ 425 | public function getLanguageByIsoCode2b(string $code): ?array 426 | { 427 | return $this->code2bToLang[$this->toLower($code)] ?? null; 428 | } 429 | 430 | /** 431 | * Get ISO-639-2t code from ISO-639-1 code 432 | */ 433 | public function code2tByCode1(string $code): string 434 | { 435 | return $this->code1ToCode2t[$this->toLower($code)] ?? ''; 436 | } 437 | 438 | } 439 | -------------------------------------------------------------------------------- /tests/ISO639Test.php: -------------------------------------------------------------------------------- 1 | iso = new ISO639(); 13 | } 14 | 15 | public static function languageByCode1DataProvider(): array 16 | { 17 | return [ 18 | // Happy path 19 | ['en', 'English'], 20 | ['fr', 'French'], 21 | ['es', 'Spanish'], 22 | ['id', 'Indonesian'], 23 | ['jv', 'Javanese'], 24 | ['hi', 'Hindi'], 25 | ['th', 'Thai'], 26 | ['ko', 'Korean'], 27 | ['ja', 'Japanese'], 28 | ['zh', 'Chinese'], 29 | ['ru', 'Russian'], 30 | ['ar', 'Arabic'], 31 | ['vi', 'Vietnamese'], 32 | ['ms', 'Malay'], 33 | ['su', 'Sundanese'], 34 | 35 | // Edge cases with spaces and tabs/newlines 36 | [' en ', 'English'], 37 | [' fr ', 'French'], 38 | ["\tes\t", 'Spanish'], 39 | ["\nid\n", 'Indonesian'], 40 | 41 | // Edge cases with multi case 42 | ['EN', 'English'], 43 | ['Fr', 'French'], 44 | ['eS', 'Spanish'], 45 | ['iD', 'Indonesian'], 46 | 47 | // Invalid 48 | ['xx', ''], 49 | ['abc', ''], 50 | ['eng', ''], 51 | 52 | // Empty 53 | ['', ''], 54 | ]; 55 | } 56 | 57 | /** @dataProvider languageByCode1DataProvider */ 58 | #[\PHPUnit\Framework\Attributes\DataProvider('languageByCode1DataProvider')] 59 | public function testLanguageByCode1(string $code, string $expected): void 60 | { 61 | $this->assertSame($expected, $this->iso->languageByCode1($code)); 62 | } 63 | 64 | public static function nativeByCode1DataProvider(): array 65 | { 66 | return [ 67 | // Default not capitalized 68 | ['en', 'English', false], 69 | ['fr', 'français, langue française', false], 70 | ['es', 'español', false], 71 | ['id', 'Bahasa Indonesia', false], 72 | ['jv', 'basa Jawa', false], 73 | ['hi', 'हिन्दी, हिंदी', false], 74 | ['th', 'ไทย', false], 75 | ['ko', '한국어', false], 76 | ['ja', '日本語 (にほんご)', false], 77 | ['zh', '中文 (Zhōngwén), 汉语, 漢語', false], 78 | ['ru', 'Русский', false], 79 | ['ar', 'العربية', false], 80 | ['vi', 'Việt Nam', false], 81 | ['ms', 'bahasa Melayu, بهاس ملايو‎', false], 82 | ['su', 'Basa Sunda', false], 83 | 84 | // Capitalized 85 | ['en', 'English', true], 86 | ['fr', 'Français, Langue Française', true], 87 | ['es', 'Español', true], 88 | ['id', 'Bahasa Indonesia', true], 89 | ['jv', 'Basa Jawa', true], 90 | ['hi', 'हिन्दी, हिंदी', true], 91 | ['th', 'ไทย', true], 92 | ['ko', '한국어', true], 93 | ['ja', '日本語 (にほんご)', true], 94 | ['zh', '中文 (Zhōngwén), 汉语, 漢語', true], 95 | ['ru', 'Русский', true], 96 | ['ar', 'العربية', true], 97 | ['vi', 'Việt Nam', true], 98 | ['ms', 'Bahasa Melayu, بهاس ملايو‎', true], 99 | ['su', 'Basa Sunda', true], 100 | 101 | // Edge cases with spaces and tabs/newlines 102 | [' en ', 'English', false], 103 | [' fr ', 'français, langue française', false], 104 | ["\tes\t", 'español', false], 105 | ["\nid\n", 'Bahasa Indonesia', false], 106 | 107 | [' en ', 'English', true], 108 | [' fr ', 'Français, Langue Française', true], 109 | ["\tes\t", 'Español', true], 110 | ["\nid\n", 'Bahasa Indonesia', true], 111 | 112 | // Edge cases with multi case 113 | ['EN', 'English', false], 114 | ['Fr', 'français, langue française', false], 115 | ['eS', 'español', false], 116 | ['iD', 'Bahasa Indonesia', false], 117 | 118 | ['EN', 'English', true], 119 | ['Fr', 'Français, Langue Française', true], 120 | ['eS', 'Español', true], 121 | ['iD', 'Bahasa Indonesia', true], 122 | 123 | // Invalid 124 | ['xx', '', false], 125 | ['abc', '', false], 126 | ['eng', '', false], 127 | 128 | ['xx', '', true], 129 | ['abc', '', true], 130 | ['eng', '', true], 131 | 132 | // Empty 133 | ['', '', false], 134 | ['', '', true], 135 | ]; 136 | } 137 | 138 | /** @dataProvider nativeByCode1DataProvider */ 139 | #[\PHPUnit\Framework\Attributes\DataProvider('nativeByCode1DataProvider')] 140 | public function testNativeByCode1(string $code, string $expected, bool $sCapitalized): void 141 | { 142 | $this->assertSame($expected, $this->iso->nativeByCode1($code, $sCapitalized)); 143 | } 144 | 145 | public static function languageByCode2tDataProvider(): array 146 | { 147 | return [ 148 | // Happy path 149 | ['eng', 'English'], 150 | ['fra', 'French'], 151 | ['spa', 'Spanish'], 152 | ['ind', 'Indonesian'], 153 | ['jav', 'Javanese'], 154 | ['hin', 'Hindi'], 155 | ['tha', 'Thai'], 156 | ['kor', 'Korean'], 157 | ['jpn', 'Japanese'], 158 | ['zho', 'Chinese'], 159 | ['rus', 'Russian'], 160 | ['ara', 'Arabic'], 161 | ['vie', 'Vietnamese'], 162 | ['msa', 'Malay'], 163 | ['sun', 'Sundanese'], 164 | 165 | // Edge cases with spaces and tabs/newlines 166 | [' zho ', 'Chinese'], 167 | [' msa ', 'Malay'], 168 | ["\tzho\t", 'Chinese'], 169 | ["\nmsa\n", 'Malay'], 170 | 171 | // Edge cases with multi case 172 | ['ENG', 'English'], 173 | ['Fra', 'French'], 174 | ['sPA', 'Spanish'], 175 | ['iND', 'Indonesian'], 176 | 177 | // Invalid 178 | ['xxx', ''], 179 | ['abc', ''], 180 | ['en', ''], 181 | 182 | // Empty 183 | ['', ''], 184 | ]; 185 | } 186 | 187 | /** @dataProvider languageByCode2tDataProvider */ 188 | #[\PHPUnit\Framework\Attributes\DataProvider('languageByCode2tDataProvider')] 189 | public function testLanguageISO6392t(string $code, string $expected): void 190 | { 191 | $this->assertSame($expected, $this->iso->languageByCode2t($code)); 192 | } 193 | 194 | public static function nativeByCode2tDataProvider(): array 195 | { 196 | return [ 197 | // Default not capitalized 198 | ['eng', 'English', false], 199 | ['fra', 'français, langue française', false], 200 | ['spa', 'español', false], 201 | ['ind', 'Bahasa Indonesia', false], 202 | ['jav', 'basa Jawa', false], 203 | ['hin', 'हिन्दी, हिंदी', false], 204 | ['tha', 'ไทย', false], 205 | ['kor', '한국어', false], 206 | ['jpn', '日本語 (にほんご)', false], 207 | ['zho', '中文 (Zhōngwén), 汉语, 漢語', false], 208 | ['rus', 'Русский', false], 209 | ['ara', 'العربية', false], 210 | ['vie', 'Việt Nam', false], 211 | ['msa', 'bahasa Melayu, بهاس ملايو‎', false], 212 | ['sun', 'Basa Sunda', false], 213 | 214 | // Capitalized 215 | ['eng', 'English', true], 216 | ['fra', 'Français, Langue Française', true], 217 | ['spa', 'Español', true], 218 | ['ind', 'Bahasa Indonesia', true], 219 | ['jav', 'Basa Jawa', true], 220 | ['hin', 'हिन्दी, हिंदी', true], 221 | ['tha', 'ไทย', true], 222 | ['kor', '한국어', true], 223 | ['jpn', '日本語 (にほんご)', true], 224 | ['zho', '中文 (Zhōngwén), 汉语, 漢語', true], 225 | ['rus', 'Русский', true], 226 | ['ara', 'العربية', true], 227 | ['vie', 'Việt Nam', true], 228 | ['msa', 'Bahasa Melayu, بهاس ملايو‎', true], 229 | ['sun', 'Basa Sunda', true], 230 | 231 | // Edge cases with spaces and tabs/newlines 232 | [' zho ', '中文 (Zhōngwén), 汉语, 漢語', false], 233 | [' msa ', 'bahasa Melayu, بهاس ملايو‎', false], 234 | ["\tzho\t", '中文 (Zhōngwén), 汉语, 漢語', false], 235 | ["\nmsa\n", 'bahasa Melayu, بهاس ملايو‎', false], 236 | 237 | [' zho ', '中文 (Zhōngwén), 汉语, 漢語', true], 238 | [' msa ', 'Bahasa Melayu, بهاس ملايو‎', true], 239 | ["\tzho\t", '中文 (Zhōngwén), 汉语, 漢語', true], 240 | ["\nmsa\n", 'Bahasa Melayu, بهاس ملايو‎', true], 241 | 242 | // Edge cases with multi case 243 | ['ENG', 'English', false], 244 | ['Fra', 'français, langue française', false], 245 | ['sPA', 'español', false], 246 | ['iND', 'Bahasa Indonesia', false], 247 | 248 | ['ENG', 'English', true], 249 | ['Fra', 'Français, Langue Française', true], 250 | ['sPA', 'Español', true], 251 | ['iND', 'Bahasa Indonesia', true], 252 | 253 | // Invalid 254 | ['xxx', '', false], 255 | ['abc', '', false], 256 | ['en', '', false], 257 | 258 | ['xxx', '', true], 259 | ['abc', '', true], 260 | ['en', '', true], 261 | 262 | // Empty 263 | ['', '', false], 264 | ['', '', true], 265 | ]; 266 | } 267 | 268 | /** @dataProvider nativeByCode2tDataProvider */ 269 | #[\PHPUnit\Framework\Attributes\DataProvider('nativeByCode2tDataProvider')] 270 | public function testNativeISO6392t(string $code, string $expected, bool $isCapitalized): void 271 | { 272 | $this->assertSame($expected, $this->iso->nativeByCode2t($code, $isCapitalized)); 273 | } 274 | 275 | public static function languageByCode2bDataProvider(): array 276 | { 277 | return [ 278 | // Happy path 279 | ['eng', 'English'], 280 | ['fre', 'French'], 281 | ['spa', 'Spanish'], 282 | ['ind', 'Indonesian'], 283 | ['jav', 'Javanese'], 284 | ['hin', 'Hindi'], 285 | ['tha', 'Thai'], 286 | ['kor', 'Korean'], 287 | ['jpn', 'Japanese'], 288 | ['chi', 'Chinese'], 289 | ['rus', 'Russian'], 290 | ['ara', 'Arabic'], 291 | ['vie', 'Vietnamese'], 292 | ['may', 'Malay'], 293 | ['sun', 'Sundanese'], 294 | 295 | // Edge cases with spaces and tabs/newlines 296 | [' chi ', 'Chinese'], 297 | [' may ', 'Malay'], 298 | ["\tchi\t", 'Chinese'], 299 | ["\nmay\n", 'Malay'], 300 | 301 | // Edge cases with multi case 302 | ['ENG', 'English'], 303 | ['Fre', 'French'], 304 | ['sPA', 'Spanish'], 305 | ['iND', 'Indonesian'], 306 | 307 | // Invalid 308 | ['xxx', ''], 309 | ['abc', ''], 310 | ['en', ''], 311 | 312 | // Empty 313 | ['', ''], 314 | ]; 315 | } 316 | 317 | /** @dataProvider languageByCode2bDataProvider */ 318 | #[\PHPUnit\Framework\Attributes\DataProvider('languageByCode2bDataProvider')] 319 | public function testLanguageByCode2b(string $code, string $expected): void 320 | { 321 | $this->assertSame($expected, $this->iso->languageByCode2b($code)); 322 | } 323 | 324 | public static function nativeByCode2bDataProvider(): array 325 | { 326 | return [ 327 | // Default not capitalized 328 | ['eng', 'English', false], 329 | ['fre', 'français, langue française', false], 330 | ['spa', 'español', false], 331 | ['ind', 'Bahasa Indonesia', false], 332 | ['jav', 'basa Jawa', false], 333 | ['hin', 'हिन्दी, हिंदी', false], 334 | ['tha', 'ไทย', false], 335 | ['kor', '한국어', false], 336 | ['jpn', '日本語 (にほんご)', false], 337 | ['chi', '中文 (Zhōngwén), 汉语, 漢語', false], 338 | ['rus', 'Русский', false], 339 | ['ara', 'العربية', false], 340 | ['vie', 'Việt Nam', false], 341 | ['may', 'bahasa Melayu, بهاس ملايو‎', false], 342 | ['sun', 'Basa Sunda', false], 343 | 344 | // Capitalized 345 | ['eng', 'English', true], 346 | ['fre', 'Français, Langue Française', true], 347 | ['spa', 'Español', true], 348 | ['ind', 'Bahasa Indonesia', true], 349 | ['jav', 'Basa Jawa', true], 350 | ['hin', 'हिन्दी, हिंदी', true], 351 | ['tha', 'ไทย', true], 352 | ['kor', '한국어', true], 353 | ['jpn', '日本語 (にほんご)', true], 354 | ['chi', '中文 (Zhōngwén), 汉语, 漢語', true], 355 | ['rus', 'Русский', true], 356 | ['ara', 'العربية', true], 357 | ['vie', 'Việt Nam', true], 358 | ['may', 'Bahasa Melayu, بهاس ملايو‎', true], 359 | ['sun', 'Basa Sunda', true], 360 | 361 | // Edge cases with spaces and tabs/newlines 362 | [' chi ', '中文 (Zhōngwén), 汉语, 漢語', false], 363 | [' may ', 'bahasa Melayu, بهاس ملايو‎', false], 364 | ["\tchi\t", '中文 (Zhōngwén), 汉语, 漢語', false], 365 | ["\nmay\n", 'bahasa Melayu, بهاس ملايو‎', false], 366 | 367 | [' chi ', '中文 (Zhōngwén), 汉语, 漢語', true], 368 | [' may ', 'Bahasa Melayu, بهاس ملايو‎', true], 369 | ["\tchi\t", '中文 (Zhōngwén), 汉语, 漢語', true], 370 | ["\nmay\n", 'Bahasa Melayu, بهاس ملايو‎', true], 371 | 372 | // Edge cases with multi case 373 | ['ENG', 'English', false], 374 | ['Fre', 'français, langue française', false], 375 | ['sPA', 'español', false], 376 | ['iND', 'Bahasa Indonesia', false], 377 | 378 | ['ENG', 'English', true], 379 | ['Fre', 'Français, Langue Française', true], 380 | ['sPA', 'Español', true], 381 | ['iND', 'Bahasa Indonesia', true], 382 | 383 | // Invalid 384 | ['xxx', '', false], 385 | ['abc', '', false], 386 | ['en', '', false], 387 | 388 | ['xxx', '', true], 389 | ['abc', '', true], 390 | ['en', '', true], 391 | 392 | // Empty 393 | ['', '', false], 394 | ['', '', true], 395 | ]; 396 | } 397 | 398 | /** @dataProvider nativeByCode2bDataProvider */ 399 | #[\PHPUnit\Framework\Attributes\DataProvider('nativeByCode2bDataProvider')] 400 | public function testNativeByCode2b(string $code, string $expected, bool $isCapitalized): void 401 | { 402 | $this->assertSame($expected, $this->iso->nativeByCode2b($code, $isCapitalized)); 403 | } 404 | 405 | public static function languageByCode3DataProvider(): array 406 | { 407 | return [ 408 | // Happy path 409 | ['eng', 'English'], 410 | ['fra', 'French'], 411 | ['spa', 'Spanish'], 412 | ['ind', 'Indonesian'], 413 | ['jav', 'Javanese'], 414 | ['hin', 'Hindi'], 415 | ['tha', 'Thai'], 416 | ['kor', 'Korean'], 417 | ['jpn', 'Japanese'], 418 | ['zho', 'Chinese'], 419 | ['rus', 'Russian'], 420 | ['ara', 'Arabic'], 421 | ['vie', 'Vietnamese'], 422 | ['msa', 'Malay'], 423 | ['sun', 'Sundanese'], 424 | 425 | // Edge cases with spaces and tabs/newlines 426 | [' zho ', 'Chinese'], 427 | [' msa ', 'Malay'], 428 | ["\tzho\t", 'Chinese'], 429 | ["\nmsa\n", 'Malay'], 430 | 431 | // Edge cases with multi case 432 | ['ENG', 'English'], 433 | ['Fra', 'French'], 434 | ['sPA', 'Spanish'], 435 | ['iND', 'Indonesian'], 436 | 437 | // Invalid 438 | ['xxx', ''], 439 | ['abc', ''], 440 | ['en', ''], 441 | 442 | // Empty 443 | ['', ''], 444 | ]; 445 | } 446 | 447 | /** @dataProvider languageByCode3DataProvider */ 448 | #[\PHPUnit\Framework\Attributes\DataProvider('languageByCode3DataProvider')] 449 | public function testLanguageByCode3(string $code, string $expected): void 450 | { 451 | $this->assertSame($expected, $this->iso->languageByCode3($code)); 452 | } 453 | 454 | public static function nativeByCode3DataProvider(): array 455 | { 456 | return [ 457 | // Default not capitalized 458 | ['eng', 'English', false], 459 | ['fra', 'français, langue française', false], 460 | ['spa', 'español', false], 461 | ['ind', 'Bahasa Indonesia', false], 462 | ['jav', 'basa Jawa', false], 463 | ['hin', 'हिन्दी, हिंदी', false], 464 | ['tha', 'ไทย', false], 465 | ['kor', '한국어', false], 466 | ['jpn', '日本語 (にほんご)', false], 467 | ['zho', '中文 (Zhōngwén), 汉语, 漢語', false], 468 | ['rus', 'Русский', false], 469 | ['ara', 'العربية', false], 470 | ['vie', 'Việt Nam', false], 471 | ['msa', 'bahasa Melayu, بهاس ملايو‎', false], 472 | ['sun', 'Basa Sunda', false], 473 | 474 | // Capitalized 475 | ['eng', 'English', true], 476 | ['fra', 'Français, Langue Française', true], 477 | ['spa', 'Español', true], 478 | ['ind', 'Bahasa Indonesia', true], 479 | ['jav', 'Basa Jawa', true], 480 | ['hin', 'हिन्दी, हिंदी', true], 481 | ['tha', 'ไทย', true], 482 | ['kor', '한국어', true], 483 | ['jpn', '日本語 (にほんご)', true], 484 | ['zho', '中文 (Zhōngwén), 汉语, 漢語', true], 485 | ['rus', 'Русский', true], 486 | ['ara', 'العربية', true], 487 | ['vie', 'Việt Nam', true], 488 | ['msa', 'Bahasa Melayu, بهاس ملايو‎', true], 489 | ['sun', 'Basa Sunda', true], 490 | 491 | // Edge cases with spaces and tabs/newlines 492 | [' zho ', '中文 (Zhōngwén), 汉语, 漢語', false], 493 | [' msa ', 'bahasa Melayu, بهاس ملايو‎', false], 494 | ["\tzho\t", '中文 (Zhōngwén), 汉语, 漢語', false], 495 | ["\nmsa\n", 'bahasa Melayu, بهاس ملايو‎', false], 496 | 497 | [' zho ', '中文 (Zhōngwén), 汉语, 漢語', true], 498 | [' msa ', 'Bahasa Melayu, بهاس ملايو‎', true], 499 | ["\tzho\t", '中文 (Zhōngwén), 汉语, 漢語', true], 500 | ["\nmsa\n", 'Bahasa Melayu, بهاس ملايو‎', true], 501 | 502 | // Edge cases with multi case 503 | ['ENG', 'English', false], 504 | ['Fra', 'français, langue française', false], 505 | ['sPA', 'español', false], 506 | ['iND', 'Bahasa Indonesia', false], 507 | 508 | ['ENG', 'English', true], 509 | ['Fra', 'Français, Langue Française', true], 510 | ['sPA', 'Español', true], 511 | ['iND', 'Bahasa Indonesia', true], 512 | 513 | // Invalid 514 | ['xxx', '', false], 515 | ['abc', '', false], 516 | ['en', '', false], 517 | 518 | ['xxx', '', true], 519 | ['abc', '', true], 520 | ['en', '', true], 521 | 522 | // Empty 523 | ['', '', false], 524 | ['', '', true], 525 | ]; 526 | } 527 | 528 | /** @dataProvider nativeByCode3DataProvider */ 529 | #[\PHPUnit\Framework\Attributes\DataProvider('nativeByCode3DataProvider')] 530 | public function testNativeByCode3(string $code, string $expected, bool $isCapitalized): void 531 | { 532 | $this->assertSame($expected, $this->iso->nativeByCode3($code, $isCapitalized)); 533 | } 534 | 535 | public static function code1ByLanguageDataProvider(): array 536 | { 537 | return [ 538 | // Happy path 539 | ['en', 'English'], 540 | ['fr', 'French'], 541 | ['es', 'Spanish'], 542 | ['id', 'Indonesian'], 543 | ['jv', 'Javanese'], 544 | ['hi', 'Hindi'], 545 | ['th', 'Thai'], 546 | ['ko', 'Korean'], 547 | ['ja', 'Japanese'], 548 | ['zh', 'Chinese'], 549 | ['ru', 'Russian'], 550 | ['ar', 'Arabic'], 551 | ['vi', 'Vietnamese'], 552 | ['ms', 'Malay'], 553 | ['su', 'Sundanese'], 554 | 555 | // Edge cases with leading/trailing spaces and tabs/newlines 556 | ['zh', ' Chinese '], 557 | ['ms', ' Malay '], 558 | ['zh', "\tChinese\t"], 559 | ['ms', "\nMalay\n"], 560 | 561 | // Edge cases with multi case 562 | ['en', 'ENGLISH'], 563 | ['fr', 'FRench'], 564 | 565 | // Invalid 566 | ['', ''], 567 | ['', 'UnknownLanguage'], 568 | ['', 'Eng'], 569 | 570 | // Empty 571 | ['', ''], 572 | ]; 573 | } 574 | /** @dataProvider code1ByLanguageDataProvider */ 575 | #[\PHPUnit\Framework\Attributes\DataProvider('code1ByLanguageDataProvider')] 576 | public function testCode1ByLanguage(string $expected, string $language): void 577 | { 578 | $this->assertSame($expected, $this->iso->code1ByLanguage($language)); 579 | } 580 | 581 | public static function code2tByLanguageDataProvider(): array 582 | { 583 | return [ 584 | // Happy path 585 | ['eng', 'English'], 586 | ['fra', 'French'], 587 | ['spa', 'Spanish'], 588 | ['ind', 'Indonesian'], 589 | ['jav', 'Javanese'], 590 | ['hin', 'Hindi'], 591 | ['tha', 'Thai'], 592 | ['kor', 'Korean'], 593 | ['jpn', 'Japanese'], 594 | ['zho', 'Chinese'], 595 | ['rus', 'Russian'], 596 | ['ara', 'Arabic'], 597 | ['vie', 'Vietnamese'], 598 | ['msa', 'Malay'], 599 | ['sun', 'Sundanese'], 600 | 601 | // Edge cases with leading/trailing spaces and tabs/newlines 602 | ['zho', ' Chinese '], 603 | ['msa', ' Malay '], 604 | ['zho', "\tChinese\t"], 605 | ['msa', "\nMalay\n"], 606 | 607 | // Edge cases with multi case 608 | ['eng', 'ENGLISH'], 609 | ['fra', 'FRench'], 610 | 611 | // Invalid 612 | ['', ''], 613 | ['', 'UnknownLanguage'], 614 | ['', 'Eng'], 615 | 616 | // Empty 617 | ['', ''], 618 | ]; 619 | } 620 | 621 | /** @dataProvider code2tByLanguageDataProvider */ 622 | #[\PHPUnit\Framework\Attributes\DataProvider('code2tByLanguageDataProvider')] 623 | public function testCode2tByLanguage(string $expected, string $language): void 624 | { 625 | $this->assertSame($expected, $this->iso->code2tByLanguage($language)); 626 | } 627 | 628 | public static function code2bByLanguageDataProvider(): array 629 | { 630 | return [ 631 | // Happy path 632 | ['eng', 'English'], 633 | ['fre', 'French'], 634 | ['spa', 'Spanish'], 635 | ['ind', 'Indonesian'], 636 | ['jav', 'Javanese'], 637 | ['hin', 'Hindi'], 638 | ['tha', 'Thai'], 639 | ['kor', 'Korean'], 640 | ['jpn', 'Japanese'], 641 | ['chi', 'Chinese'], 642 | ['rus', 'Russian'], 643 | ['ara', 'Arabic'], 644 | ['vie', 'Vietnamese'], 645 | ['may', 'Malay'], 646 | ['sun', 'Sundanese'], 647 | 648 | // Edge cases with leading/trailing spaces and tabs/newlines 649 | ['chi', ' Chinese '], 650 | ['may', ' Malay '], 651 | ['chi', "\tChinese\t"], 652 | ['may', "\nMalay\n"], 653 | 654 | // Edge cases with multi case 655 | ['eng', 'ENGLISH'], 656 | ['fre', 'FRench'], 657 | 658 | // Invalid 659 | ['', ''], 660 | ['', 'UnknownLanguage'], 661 | ['', 'Eng'], 662 | 663 | // Empty 664 | ['', ''], 665 | ]; 666 | } 667 | 668 | /** @dataProvider code2bByLanguageDataProvider */ 669 | #[\PHPUnit\Framework\Attributes\DataProvider('code2bByLanguageDataProvider')] 670 | public function testCode2bByLanguage(string $expected, string $language): void 671 | { 672 | $this->assertSame($expected, $this->iso->code2bByLanguage($language)); 673 | } 674 | 675 | public static function code3ByLanguageDataProvider(): array 676 | { 677 | return [ 678 | // Happy path 679 | ['eng', 'English'], 680 | ['fra', 'French'], 681 | ['spa', 'Spanish'], 682 | ['ind', 'Indonesian'], 683 | ['jav', 'Javanese'], 684 | ['hin', 'Hindi'], 685 | ['tha', 'Thai'], 686 | ['kor', 'Korean'], 687 | ['jpn', 'Japanese'], 688 | ['zho', 'Chinese'], 689 | ['rus', 'Russian'], 690 | ['ara', 'Arabic'], 691 | ['vie', 'Vietnamese'], 692 | ['msa', 'Malay'], 693 | ['sun', 'Sundanese'], 694 | 695 | // Edge cases with leading/trailing spaces and tabs/newlines 696 | ['zho', ' Chinese '], 697 | ['msa', ' Malay '], 698 | ['zho', "\tChinese\t"], 699 | ['msa', "\nMalay\n"], 700 | 701 | // Edge cases with multi case 702 | ['eng', 'ENGLISH'], 703 | ['fra', 'FRench'], 704 | 705 | // Invalid 706 | ['', ''], 707 | ['', 'UnknownLanguage'], 708 | ['', 'Eng'], 709 | 710 | // Empty 711 | ['', ''], 712 | ]; 713 | } 714 | 715 | /** @dataProvider code3ByLanguageDataProvider */ 716 | #[\PHPUnit\Framework\Attributes\DataProvider('code3ByLanguageDataProvider')] 717 | public function testCode3ByLanguage(string $expected, string $language): void 718 | { 719 | $this->assertSame($expected, $this->iso->code3ByLanguage($language)); 720 | } 721 | 722 | public static function getLanguageByIsoCode2bDataProvider(): array 723 | { 724 | return [ 725 | [['en', 'eng', 'eng', 'eng', 'English', 'English'], 'eng'], 726 | [['fr', 'fra', 'fre', 'fra', 'French', 'français, langue française'], 'fre'], 727 | [['id', 'ind', 'ind', 'ind', 'Indonesian', 'Bahasa Indonesia'], 'ind'], 728 | ]; 729 | } 730 | 731 | /** @dataProvider getLanguageByIsoCode2bDataProvider */ 732 | #[\PHPUnit\Framework\Attributes\DataProvider('getLanguageByIsoCode2bDataProvider')] 733 | public function testGetLanguageByIsoCode2B(array $expected, string $code): void 734 | { 735 | $this->assertSame($expected, $this->iso->getLanguageByIsoCode2b($code)); 736 | } 737 | 738 | public static function getLanguageByIsoCode2bNullDataProvider(): array 739 | { 740 | return [ 741 | ['null'], 742 | ['abc'], 743 | ]; 744 | } 745 | 746 | /** @dataProvider getLanguageByIsoCode2bNullDataProvider */ 747 | #[\PHPUnit\Framework\Attributes\DataProvider('getLanguageByIsoCode2bNullDataProvider')] 748 | public function testGetLanguageByIsoCode2bNull(string $code): void 749 | { 750 | $this->assertNull($this->iso->getLanguageByIsoCode2b($code)); 751 | } 752 | 753 | public static function code2tByCode1DataProvider(): array 754 | { 755 | return [ 756 | ['fra', 'fr'], 757 | ['eng', 'en'], 758 | ['spa', 'es'], 759 | ['ind', 'id'], 760 | ]; 761 | } 762 | 763 | /** @dataProvider code2tByCode1DataProvider */ 764 | #[\PHPUnit\Framework\Attributes\DataProvider('code2tByCode1DataProvider')] 765 | public function testCode2tByCode1(string $expected, string $code): void 766 | { 767 | $this->assertSame($expected, $this->iso->code2tByCode1($code)); 768 | } 769 | 770 | // Test allLanguages method 771 | public function testAllLanguages(): void 772 | { 773 | $languages = $this->iso->allLanguages(); 774 | $this->assertIsArray($languages); 775 | $this->assertNotEmpty($languages); 776 | 777 | // Check that each language entry has 6 elements (ISO-639-1, ISO-639-2t, ISO-639-2b, ISO-639-3, English, Native) 778 | foreach ($languages as $language) { 779 | $this->assertIsArray($language); 780 | $this->assertCount(6, $language); 781 | } 782 | 783 | // Test that it contains some expected languages 784 | $englishFound = false; 785 | $frenchFound = false; 786 | 787 | foreach ($languages as $language) { 788 | if ($language[0] === 'en' && $language[4] === 'English') { 789 | $englishFound = true; 790 | } 791 | if ($language[0] === 'fr' && $language[4] === 'French') { 792 | $frenchFound = true; 793 | } 794 | } 795 | 796 | $this->assertTrue($englishFound, 'English language should be found in the languages array'); 797 | $this->assertTrue($frenchFound, 'French language should be found in the languages array'); 798 | } 799 | 800 | public static function consistencyDataProvider(): array 801 | { 802 | return [ 803 | ['en', 'eng', 'eng', 'eng', 'English'], 804 | ['fr', 'fra', 'fre', 'fra', 'French'], 805 | ['es', 'spa', 'spa', 'spa', 'Spanish'], 806 | ['id', 'ind', 'ind', 'ind', 'Indonesian'], 807 | ['de', 'deu', 'ger', 'deu', 'German'], 808 | ]; 809 | } 810 | 811 | /** @dataProvider consistencyDataProvider */ 812 | #[\PHPUnit\Framework\Attributes\DataProvider('consistencyDataProvider')] 813 | public function testConsistencyBetweenCodeFormats(string $code1, string $code2t, string $code2b, string $code3, string $expectedEnglish): void 814 | { 815 | // Test that all code formats return the same English name 816 | $this->assertSame($expectedEnglish, $this->iso->languageByCode1($code1)); 817 | $this->assertSame($expectedEnglish, $this->iso->languageByCode2t($code2t)); 818 | $this->assertSame($expectedEnglish, $this->iso->languageByCode2b($code2b)); 819 | $this->assertSame($expectedEnglish, $this->iso->languageByCode3($code3)); 820 | 821 | // Test reverse lookups 822 | $this->assertSame($code1, $this->iso->code1ByLanguage($expectedEnglish)); 823 | $this->assertSame($code2t, $this->iso->code2tByLanguage($expectedEnglish)); 824 | $this->assertSame($code2b, $this->iso->code2bByLanguage($expectedEnglish)); 825 | $this->assertSame($code3, $this->iso->code3ByLanguage($expectedEnglish)); 826 | 827 | // Test code conversions 828 | $this->assertSame($code2t, $this->iso->code2tByCode1($code1)); 829 | } 830 | 831 | public function testSpecialCases(): void 832 | { 833 | // Ladin language only has ISO 639-3 code 834 | $this->assertSame('lld', $this->iso->code3ByLanguage('Ladin')); 835 | $this->assertSame('Ladin', $this->iso->languageByCode3('lld')); 836 | } 837 | 838 | } 839 | --------------------------------------------------------------------------------